package s3api

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"testing"

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

// NOTE: These are integration tests that test the end-to-end encryption/decryption flow.
// Full HTTP handler tests (PUT -> GET) would require a complete mock server with filer,
// which is complex to set up. These tests focus on the critical decrypt path.

// TestSSES3EndToEndSmallFile tests the complete encryption->storage->decryption cycle for small inline files
// This test would have caught the IV retrieval bug for inline files
func TestSSES3EndToEndSmallFile(t *testing.T) {
	// Initialize global SSE-S3 key manager
	globalSSES3KeyManager = NewSSES3KeyManager()
	defer func() {
		globalSSES3KeyManager = NewSSES3KeyManager()
	}()

	// Set up the key manager with a super key for testing
	keyManager := GetSSES3KeyManager()
	keyManager.superKey = make([]byte, 32)
	for i := range keyManager.superKey {
		keyManager.superKey[i] = byte(i)
	}

	testCases := []struct {
		name string
		data []byte
	}{
		{"tiny file (10 bytes)", []byte("test12345")},
		{"small file (50 bytes)", []byte("This is a small test file for SSE-S3 encryption")},
		{"medium file (256 bytes)", bytes.Repeat([]byte("a"), 256)},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Step 1: Encrypt (simulates what happens during PUT)
			sseS3Key, err := GenerateSSES3Key()
			if err != nil {
				t.Fatalf("Failed to generate SSE-S3 key: %v", err)
			}

			encryptedReader, iv, err := CreateSSES3EncryptedReader(bytes.NewReader(tc.data), sseS3Key)
			if err != nil {
				t.Fatalf("Failed to create encrypted reader: %v", err)
			}

			encryptedData, err := io.ReadAll(encryptedReader)
			if err != nil {
				t.Fatalf("Failed to read encrypted data: %v", err)
			}

			// Store IV in the key (this is critical for inline files!)
			sseS3Key.IV = iv

			// Serialize the metadata (this is stored in entry.Extended)
			serializedMetadata, err := SerializeSSES3Metadata(sseS3Key)
			if err != nil {
				t.Fatalf("Failed to serialize SSE-S3 metadata: %v", err)
			}

			// Step 2: Simulate storage (inline file - no chunks)
			// For inline files, data is in Content, metadata in Extended
			mockEntry := &filer_pb.Entry{
				Extended: map[string][]byte{
					s3_constants.SeaweedFSSSES3Key:       serializedMetadata,
					s3_constants.AmzServerSideEncryption: []byte("AES256"),
				},
				Content: encryptedData,
				Chunks:  []*filer_pb.FileChunk{}, // Critical: inline files have NO chunks
			}

			// Step 3: Decrypt (simulates what happens during GET)
			// This tests the IV retrieval path for inline files

			// First, deserialize metadata from storage
			retrievedKeyData := mockEntry.Extended[s3_constants.SeaweedFSSSES3Key]
			retrievedKey, err := DeserializeSSES3Metadata(retrievedKeyData, keyManager)
			if err != nil {
				t.Fatalf("Failed to deserialize SSE-S3 metadata: %v", err)
			}

			// CRITICAL TEST: For inline files, IV must be in object-level metadata
			var retrievedIV []byte
			if len(retrievedKey.IV) > 0 {
				// Success path: IV found in object-level key
				retrievedIV = retrievedKey.IV
			} else if len(mockEntry.GetChunks()) > 0 {
				// Fallback path: would check chunks (but inline files have no chunks)
				t.Fatal("Inline file should have IV in object-level metadata, not chunks")
			}

			if len(retrievedIV) == 0 {
				// THIS IS THE BUG WE FIXED: inline files had no way to get IV!
				t.Fatal("Failed to retrieve IV for inline file - this is the bug we fixed!")
			}

			// Now decrypt with the retrieved IV
			decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(encryptedData), retrievedKey, retrievedIV)
			if err != nil {
				t.Fatalf("Failed to create decrypted reader: %v", err)
			}

			decryptedData, err := io.ReadAll(decryptedReader)
			if err != nil {
				t.Fatalf("Failed to read decrypted data: %v", err)
			}

			// Verify decrypted data matches original
			if !bytes.Equal(decryptedData, tc.data) {
				t.Errorf("Decrypted data doesn't match original.\nExpected: %q\nGot: %q", tc.data, decryptedData)
			}
		})
	}
}

// TestSSES3EndToEndChunkedFile tests the complete flow for chunked files
func TestSSES3EndToEndChunkedFile(t *testing.T) {
	// Initialize global SSE-S3 key manager
	globalSSES3KeyManager = NewSSES3KeyManager()
	defer func() {
		globalSSES3KeyManager = NewSSES3KeyManager()
	}()

	keyManager := GetSSES3KeyManager()
	keyManager.superKey = make([]byte, 32)
	for i := range keyManager.superKey {
		keyManager.superKey[i] = byte(i)
	}

	// Generate SSE-S3 key
	sseS3Key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("Failed to generate SSE-S3 key: %v", err)
	}

	// Create test data for two chunks
	chunk1Data := []byte("This is chunk 1 data for SSE-S3 encryption test")
	chunk2Data := []byte("This is chunk 2 data for SSE-S3 encryption test")

	// Encrypt chunk 1
	encryptedReader1, iv1, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk1Data), sseS3Key)
	if err != nil {
		t.Fatalf("Failed to create encrypted reader for chunk 1: %v", err)
	}
	encryptedChunk1, _ := io.ReadAll(encryptedReader1)

	// Encrypt chunk 2
	encryptedReader2, iv2, err := CreateSSES3EncryptedReader(bytes.NewReader(chunk2Data), sseS3Key)
	if err != nil {
		t.Fatalf("Failed to create encrypted reader for chunk 2: %v", err)
	}
	encryptedChunk2, _ := io.ReadAll(encryptedReader2)

	// Create metadata for each chunk
	chunk1Key := &SSES3Key{
		Key:       sseS3Key.Key,
		IV:        iv1,
		Algorithm: sseS3Key.Algorithm,
		KeyID:     sseS3Key.KeyID,
	}
	chunk2Key := &SSES3Key{
		Key:       sseS3Key.Key,
		IV:        iv2,
		Algorithm: sseS3Key.Algorithm,
		KeyID:     sseS3Key.KeyID,
	}

	serializedChunk1Meta, _ := SerializeSSES3Metadata(chunk1Key)
	serializedChunk2Meta, _ := SerializeSSES3Metadata(chunk2Key)
	serializedObjMeta, _ := SerializeSSES3Metadata(sseS3Key)

	// Create mock entry with chunks
	mockEntry := &filer_pb.Entry{
		Extended: map[string][]byte{
			s3_constants.SeaweedFSSSES3Key:       serializedObjMeta,
			s3_constants.AmzServerSideEncryption: []byte("AES256"),
		},
		Chunks: []*filer_pb.FileChunk{
			{
				FileId:      "chunk1,123",
				Offset:      0,
				Size:        uint64(len(encryptedChunk1)),
				SseType:     filer_pb.SSEType_SSE_S3,
				SseMetadata: serializedChunk1Meta,
			},
			{
				FileId:      "chunk2,456",
				Offset:      int64(len(chunk1Data)),
				Size:        uint64(len(encryptedChunk2)),
				SseType:     filer_pb.SSEType_SSE_S3,
				SseMetadata: serializedChunk2Meta,
			},
		},
	}

	// Verify multipart detection
	sses3Chunks := 0
	for _, chunk := range mockEntry.GetChunks() {
		if chunk.GetSseType() == filer_pb.SSEType_SSE_S3 && len(chunk.GetSseMetadata()) > 0 {
			sses3Chunks++
		}
	}

	isMultipart := sses3Chunks > 1
	if !isMultipart {
		t.Error("Expected multipart SSE-S3 object detection")
	}

	if sses3Chunks != 2 {
		t.Errorf("Expected 2 SSE-S3 chunks, got %d", sses3Chunks)
	}

	// Verify each chunk has valid metadata with IV
	for i, chunk := range mockEntry.GetChunks() {
		deserializedKey, err := DeserializeSSES3Metadata(chunk.GetSseMetadata(), keyManager)
		if err != nil {
			t.Errorf("Failed to deserialize chunk %d metadata: %v", i, err)
		}
		if len(deserializedKey.IV) == 0 {
			t.Errorf("Chunk %d has no IV", i)
		}

		// Decrypt this chunk to verify it works
		var chunkData []byte
		if i == 0 {
			chunkData = encryptedChunk1
		} else {
			chunkData = encryptedChunk2
		}

		decryptedReader, err := CreateSSES3DecryptedReader(bytes.NewReader(chunkData), deserializedKey, deserializedKey.IV)
		if err != nil {
			t.Errorf("Failed to decrypt chunk %d: %v", i, err)
			continue
		}

		decrypted, _ := io.ReadAll(decryptedReader)
		var expectedData []byte
		if i == 0 {
			expectedData = chunk1Data
		} else {
			expectedData = chunk2Data
		}

		if !bytes.Equal(decrypted, expectedData) {
			t.Errorf("Chunk %d decryption failed", i)
		}
	}
}

// TestSSES3EndToEndWithDetectPrimaryType tests that type detection works correctly for different scenarios
func TestSSES3EndToEndWithDetectPrimaryType(t *testing.T) {
	s3a := &S3ApiServer{}

	testCases := []struct {
		name          string
		entry         *filer_pb.Entry
		expectedType  string
		shouldBeSSES3 bool
	}{
		{
			name: "Inline SSE-S3 file (no chunks)",
			entry: &filer_pb.Entry{
				Extended: map[string][]byte{
					s3_constants.AmzServerSideEncryption: []byte("AES256"),
				},
				Attributes: &filer_pb.FuseAttributes{},
				Content:    []byte("encrypted data"),
				Chunks:     []*filer_pb.FileChunk{},
			},
			expectedType:  s3_constants.SSETypeS3,
			shouldBeSSES3: true,
		},
		{
			name: "Single chunk SSE-S3 file",
			entry: &filer_pb.Entry{
				Extended: map[string][]byte{
					s3_constants.AmzServerSideEncryption: []byte("AES256"),
				},
				Attributes: &filer_pb.FuseAttributes{},
				Chunks: []*filer_pb.FileChunk{
					{
						FileId:      "1,123",
						SseType:     filer_pb.SSEType_SSE_S3,
						SseMetadata: []byte("metadata"),
					},
				},
			},
			expectedType:  s3_constants.SSETypeS3,
			shouldBeSSES3: true,
		},
		{
			name: "SSE-KMS file (has KMS key ID)",
			entry: &filer_pb.Entry{
				Extended: map[string][]byte{
					s3_constants.AmzServerSideEncryption:            []byte("AES256"),
					s3_constants.AmzServerSideEncryptionAwsKmsKeyId: []byte("kms-key-123"),
				},
				Attributes: &filer_pb.FuseAttributes{},
				Chunks:     []*filer_pb.FileChunk{},
			},
			expectedType:  s3_constants.SSETypeKMS,
			shouldBeSSES3: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			detectedType := s3a.detectPrimarySSEType(tc.entry)
			if detectedType != tc.expectedType {
				t.Errorf("Expected type %s, got %s", tc.expectedType, detectedType)
			}
			if (detectedType == s3_constants.SSETypeS3) != tc.shouldBeSSES3 {
				t.Errorf("SSE-S3 detection mismatch: expected %v, got %v", tc.shouldBeSSES3, detectedType == s3_constants.SSETypeS3)
			}
		})
	}
}

// initSSES3KeyManagerForTest resets the global SSE-S3 key manager and seeds it
// with a deterministic super key suitable for envelope-encrypt/decrypt cycles
// inside tests. Returns the freshly initialized manager.
func initSSES3KeyManagerForTest(t *testing.T) *SSES3KeyManager {
	t.Helper()
	globalSSES3KeyManager = NewSSES3KeyManager()
	t.Cleanup(func() { globalSSES3KeyManager = NewSSES3KeyManager() })
	km := GetSSES3KeyManager()
	km.superKey = make([]byte, 32)
	for i := range km.superKey {
		km.superKey[i] = byte(i)
	}
	return km
}

// encryptSSES3Part encrypts data with a freshly generated DEK and returns the
// ciphertext plus serialized per-chunk metadata (DEK + IV). Mirrors what the
// multipart upload path writes into each part's chunk metadata.
func encryptSSES3Part(t *testing.T, data []byte) (ciphertext, metadata []byte) {
	t.Helper()
	key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("GenerateSSES3Key: %v", err)
	}
	encReader, iv, err := CreateSSES3EncryptedReader(bytes.NewReader(data), key)
	if err != nil {
		t.Fatalf("CreateSSES3EncryptedReader: %v", err)
	}
	ciphertext, err = io.ReadAll(encReader)
	if err != nil {
		t.Fatalf("ReadAll ciphertext: %v", err)
	}
	key.IV = iv
	metadata, err = SerializeSSES3Metadata(key)
	if err != nil {
		t.Fatalf("SerializeSSES3Metadata: %v", err)
	}
	return ciphertext, metadata
}

// TestBuildMultipartSSES3Reader_PerChunkKeys locks in the fix from PR #9211:
// each multipart part has its own DEK and IV, and the direct multipart reader
// must decrypt each chunk with its own per-chunk metadata (not the entry-level
// key). Before the fix, using a shared entry key produced garbled output.
func TestBuildMultipartSSES3Reader_PerChunkKeys(t *testing.T) {
	keyManager := initSSES3KeyManagerForTest(t)

	// Two parts with distinct sizes (including a short final part) and
	// independent DEKs/IVs to exercise the per-chunk key plumbing.
	part1Plaintext := bytes.Repeat([]byte("ABCDEFGHIJKLMNOP"), 16) // 256 bytes
	part2Plaintext := []byte("short tail part")                    // 15 bytes

	cipher1, meta1 := encryptSSES3Part(t, part1Plaintext)
	cipher2, meta2 := encryptSSES3Part(t, part2Plaintext)

	chunks := []*filer_pb.FileChunk{
		{
			FileId:      "1,aaa",
			Offset:      0,
			Size:        uint64(len(cipher1)),
			SseType:     filer_pb.SSEType_SSE_S3,
			SseMetadata: meta1,
		},
		{
			FileId:      "2,bbb",
			Offset:      int64(len(part1Plaintext)),
			Size:        uint64(len(cipher2)),
			SseType:     filer_pb.SSEType_SSE_S3,
			SseMetadata: meta2,
		},
	}
	// Pass chunks out of order to verify offset-based sort.
	shuffled := []*filer_pb.FileChunk{chunks[1], chunks[0]}
	// Snapshot the input ordering; the helper must not mutate the caller's
	// slice, which is backed by entry.Chunks and relied on elsewhere (e.g.
	// ETag computation).
	shuffledOrderBefore := []*filer_pb.FileChunk{shuffled[0], shuffled[1]}

	fetched := map[string]int{}
	chunkData := map[string][]byte{
		"1,aaa": cipher1,
		"2,bbb": cipher2,
	}
	fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
		fetched[c.GetFileIdString()]++
		data, ok := chunkData[c.GetFileIdString()]
		if !ok {
			return nil, fmt.Errorf("unexpected chunk %s", c.GetFileIdString())
		}
		return io.NopCloser(bytes.NewReader(data)), nil
	}

	reader, err := buildMultipartSSES3Reader(shuffled, keyManager, fetch)
	if err != nil {
		t.Fatalf("buildMultipartSSES3Reader: %v", err)
	}

	got, err := io.ReadAll(reader)
	if err != nil {
		t.Fatalf("ReadAll reader: %v", err)
	}

	want := append(append([]byte{}, part1Plaintext...), part2Plaintext...)
	if !bytes.Equal(got, want) {
		t.Fatalf("decrypted output mismatch\n want (len=%d): %q\n  got (len=%d): %q",
			len(want), want, len(got), got)
	}
	if fetched["1,aaa"] != 1 || fetched["2,bbb"] != 1 {
		t.Errorf("expected each chunk fetched once, got %v", fetched)
	}
	for i := range shuffledOrderBefore {
		if shuffled[i] != shuffledOrderBefore[i] {
			t.Errorf("caller's chunk slice was reordered at index %d: before=%s after=%s",
				i, shuffledOrderBefore[i].GetFileIdString(), shuffled[i].GetFileIdString())
		}
	}
}

// TestBuildMultipartSSES3Reader_InvalidIVLength verifies that per-chunk metadata
// with a missing or short IV is rejected with a clear error instead of
// panicking inside cipher.NewCTR. The short-IV case is crafted by encoding the
// metadata JSON directly, since SerializeSSES3Metadata refuses to emit a bad
// IV; this simulates corrupted or legacy on-disk metadata.
func TestBuildMultipartSSES3Reader_InvalidIVLength(t *testing.T) {
	keyManager := initSSES3KeyManagerForTest(t)

	// Pre-encrypt the DEK with the current super key so Deserialize can unwrap
	// it successfully and we reach the IV-length check.
	dek := bytes.Repeat([]byte{0x42}, s3_constants.AESKeySize)
	encryptedDEK, nonce, err := keyManager.encryptKeyWithSuperKey(dek)
	if err != nil {
		t.Fatalf("encryptKeyWithSuperKey: %v", err)
	}

	makeMetadata := func(iv []byte) []byte {
		meta := map[string]string{
			"algorithm":    s3_constants.SSEAlgorithmAES256,
			"keyId":        "test-key",
			"encryptedDEK": base64.StdEncoding.EncodeToString(encryptedDEK),
			"nonce":        base64.StdEncoding.EncodeToString(nonce),
		}
		if iv != nil {
			meta["iv"] = base64.StdEncoding.EncodeToString(iv)
		}
		out, err := json.Marshal(meta)
		if err != nil {
			t.Fatalf("marshal metadata: %v", err)
		}
		return out
	}

	cases := []struct {
		name string
		iv   []byte
	}{
		{"missing IV", nil},
		{"short IV", []byte("too-short")}, // 9 bytes, not 16
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			fetchCalled := false
			fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
				fetchCalled = true
				return io.NopCloser(bytes.NewReader([]byte("whatever"))), nil
			}

			chunks := []*filer_pb.FileChunk{
				{
					FileId:      "1,bad",
					Offset:      0,
					Size:        8,
					SseType:     filer_pb.SSEType_SSE_S3,
					SseMetadata: makeMetadata(tc.iv),
				},
			}

			_, err := buildMultipartSSES3Reader(chunks, keyManager, fetch)
			if err == nil {
				t.Fatal("expected error for invalid IV length, got nil")
			}
			// ValidateIV's error format is "invalid <name> length: ...";
			// match on the part of the message that's stable across the
			// shared helper's wording.
			if !strings.Contains(err.Error(), "IV length") {
				t.Errorf("expected 'IV length' in error, got: %v", err)
			}
			// Validation runs upfront before any chunk fetch, so no volume-server
			// HTTP connection should have been opened on the failure path.
			if fetchCalled {
				t.Error("fetchChunk was called for an invalid-IV chunk; metadata validation should fail before any fetch")
			}
		})
	}
}

// TestBuildMultipartSSES3Reader_RejectsBadChunkBeforeAnyFetch verifies that
// when any chunk's metadata is malformed, the helper returns an error WITHOUT
// having opened a volume-server HTTP connection for any chunk. Per-chunk
// metadata is validated upfront precisely so a bad chunk in position N does
// not leak open HTTP responses for chunks 0..N-1 (the original eager
// implementation depended on a closeAppendedReaders cleanup path; this test
// pins the stronger contract: nothing is opened in the first place).
func TestBuildMultipartSSES3Reader_RejectsBadChunkBeforeAnyFetch(t *testing.T) {
	keyManager := initSSES3KeyManagerForTest(t)

	// First chunk: valid SSE-S3 chunk.
	cipher1, meta1 := encryptSSES3Part(t, []byte("first chunk plaintext"))

	// Second chunk: missing per-chunk metadata, triggers error.
	chunks := []*filer_pb.FileChunk{
		{
			FileId:      "1,good",
			Offset:      0,
			Size:        uint64(len(cipher1)),
			SseType:     filer_pb.SSEType_SSE_S3,
			SseMetadata: meta1,
		},
		{
			FileId:      "2,bad",
			Offset:      int64(len(cipher1)),
			Size:        1,
			SseType:     filer_pb.SSEType_SSE_S3,
			SseMetadata: nil, // triggers "missing per-chunk metadata"
		},
	}

	fetched := map[string]int{}
	fetch := func(c *filer_pb.FileChunk) (io.ReadCloser, error) {
		fetched[c.GetFileIdString()]++
		return io.NopCloser(bytes.NewReader([]byte("x"))), nil
	}

	_, err := buildMultipartSSES3Reader(chunks, keyManager, fetch)
	if err == nil {
		t.Fatal("expected error from missing chunk metadata, got nil")
	}
	if !strings.Contains(err.Error(), "missing per-chunk metadata") {
		t.Errorf("expected 'missing per-chunk metadata' in error, got: %v", err)
	}
	if len(fetched) != 0 {
		t.Errorf("expected no chunks fetched on validation failure, got %v", fetched)
	}
}
