package filesystem

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/lukaszraczylo/gohoarder/pkg/storage"
	"github.com/stretchr/testify/suite"
)

type FilesystemStorageTestSuite struct {
	suite.Suite
	fs      *FilesystemStorage
	tempDir string
}

func (s *FilesystemStorageTestSuite) SetupTest() {
	var err error
	s.tempDir, err = os.MkdirTemp("", "gohoarder-test-*")
	s.Require().NoError(err)

	s.fs, err = New(s.tempDir, 1024*1024) // 1MB quota
	s.Require().NoError(err)
}

func (s *FilesystemStorageTestSuite) TearDownTest() {
	if s.fs != nil {
		s.fs.Close() // #nosec G104 -- Cleanup, error not critical
	}
	if s.tempDir != "" {
		_ = os.RemoveAll(s.tempDir) // #nosec G104 -- Cleanup
	}
}

func TestFilesystemStorageTestSuite(t *testing.T) {
	suite.Run(t, new(FilesystemStorageTestSuite))
}

// Test Put operation
func (s *FilesystemStorageTestSuite) TestPut() {
	tests := []struct {
		opts        *storage.PutOptions
		errorCheck  func(error) bool
		name        string
		key         string
		data        string
		expectError bool
	}{
		{
			name:        "successful put",
			key:         "test/file.txt",
			data:        "hello world",
			opts:        nil,
			expectError: false,
		},
		{
			name:        "put with valid MD5 checksum",
			key:         "test/checksummed.txt",
			data:        "test data",
			opts:        &storage.PutOptions{ChecksumMD5: "eb733a00c0c9d336e65691a37ab54293"},
			expectError: false,
		},
		{
			name:        "put with invalid MD5 checksum",
			key:         "test/bad-checksum.txt",
			data:        "test data",
			opts:        &storage.PutOptions{ChecksumMD5: "invalid"},
			expectError: true,
		},
		{
			name:        "put with nested path",
			key:         "deep/nested/path/file.txt",
			data:        "nested content",
			opts:        nil,
			expectError: false,
		},
		{
			name:        "put with path traversal attempt",
			key:         "../../../etc/passwd",
			data:        "malicious",
			opts:        nil,
			expectError: false, // Should be sanitized, not error
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			ctx := context.Background()
			reader := strings.NewReader(tt.data)

			err := s.fs.Put(ctx, tt.key, reader, tt.opts)

			if tt.expectError {
				s.Error(err)
			} else {
				s.NoError(err)
				// Verify file exists
				exists, err := s.fs.Exists(ctx, tt.key)
				s.NoError(err)
				s.True(exists)
			}
		})
	}
}

// Test Get operation
func (s *FilesystemStorageTestSuite) TestGet() {
	ctx := context.Background()

	// Setup: Put a test file
	testData := "test content for retrieval"
	err := s.fs.Put(ctx, "test/get.txt", strings.NewReader(testData), nil)
	s.Require().NoError(err)

	tests := []struct {
		name        string
		key         string
		expectData  string
		expectError bool
	}{
		{
			name:        "get existing file",
			key:         "test/get.txt",
			expectError: false,
			expectData:  testData,
		},
		{
			name:        "get non-existent file",
			key:         "does/not/exist.txt",
			expectError: true,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			reader, err := s.fs.Get(ctx, tt.key)

			if tt.expectError {
				s.Error(err)
				s.Nil(reader)
			} else {
				s.NoError(err)
				s.NotNil(reader)
				defer reader.Close() // #nosec G104 -- Cleanup, error not critical

				data, err := io.ReadAll(reader)
				s.NoError(err)
				s.Equal(tt.expectData, string(data))
			}
		})
	}
}

// Test Delete operation
func (s *FilesystemStorageTestSuite) TestDelete() {
	ctx := context.Background()

	tests := []struct {
		name        string
		setupKey    string
		deleteKey   string
		expectError bool
	}{
		{
			name:        "delete existing file",
			setupKey:    "test/delete-me.txt",
			deleteKey:   "test/delete-me.txt",
			expectError: false,
		},
		{
			name:        "delete non-existent file",
			setupKey:    "",
			deleteKey:   "does/not/exist.txt",
			expectError: true,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			// Setup
			if tt.setupKey != "" {
				err := s.fs.Put(ctx, tt.setupKey, strings.NewReader("to be deleted"), nil)
				s.Require().NoError(err)
			}

			// Test delete
			err := s.fs.Delete(ctx, tt.deleteKey)

			if tt.expectError {
				s.Error(err)
			} else {
				s.NoError(err)
				// Verify file no longer exists
				exists, err := s.fs.Exists(ctx, tt.deleteKey)
				s.NoError(err)
				s.False(exists)
			}
		})
	}
}

// Test Exists operation
func (s *FilesystemStorageTestSuite) TestExists() {
	ctx := context.Background()

	// Setup: Put a test file
	err := s.fs.Put(ctx, "test/exists.txt", strings.NewReader("content"), nil)
	s.Require().NoError(err)

	tests := []struct {
		name   string
		key    string
		exists bool
	}{
		{
			name:   "existing file",
			key:    "test/exists.txt",
			exists: true,
		},
		{
			name:   "non-existent file",
			key:    "test/does-not-exist.txt",
			exists: false,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			exists, err := s.fs.Exists(ctx, tt.key)
			s.NoError(err)
			s.Equal(tt.exists, exists)
		})
	}
}

// Test List operation
func (s *FilesystemStorageTestSuite) TestList() {
	ctx := context.Background()

	// Setup: Create multiple files
	files := []string{
		"packages/npm/react/17.0.1/package.json",
		"packages/npm/react/17.0.2/package.json",
		"packages/npm/vue/3.0.0/package.json",
		"packages/pypi/django/3.2.0/wheel.whl",
	}

	for _, file := range files {
		err := s.fs.Put(ctx, file, strings.NewReader("content"), nil)
		s.Require().NoError(err)
	}

	tests := []struct {
		opts          *storage.ListOptions
		name          string
		prefix        string
		expectedKeys  []string
		expectedCount int
	}{
		{
			name:          "list all npm packages",
			prefix:        "packages/npm",
			opts:          nil,
			expectedCount: 3,
		},
		{
			name:          "list react packages",
			prefix:        "packages/npm/react",
			opts:          nil,
			expectedCount: 2,
		},
		{
			name:          "list with pagination",
			prefix:        "packages/npm",
			opts:          &storage.ListOptions{MaxResults: 2, Offset: 0},
			expectedCount: 2,
		},
		{
			name:          "list with offset",
			prefix:        "packages/npm",
			opts:          &storage.ListOptions{MaxResults: 2, Offset: 1},
			expectedCount: 2,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			objects, err := s.fs.List(ctx, tt.prefix, tt.opts)
			s.NoError(err)
			s.Equal(tt.expectedCount, len(objects))

			// Verify objects have required fields
			for _, obj := range objects {
				s.NotEmpty(obj.Key)
				s.Greater(obj.Size, int64(0))
				s.False(obj.Modified.IsZero())
			}
		})
	}
}

// Test Stat operation
func (s *FilesystemStorageTestSuite) TestStat() {
	ctx := context.Background()

	// Setup: Put a test file
	testData := "stat test content"
	testKey := "test/stat.txt"
	err := s.fs.Put(ctx, testKey, strings.NewReader(testData), nil)
	s.Require().NoError(err)

	tests := []struct {
		name        string
		key         string
		expectError bool
	}{
		{
			name:        "stat existing file",
			key:         testKey,
			expectError: false,
		},
		{
			name:        "stat non-existent file",
			key:         "does/not/exist.txt",
			expectError: true,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			info, err := s.fs.Stat(ctx, tt.key)

			if tt.expectError {
				s.Error(err)
				s.Nil(info)
			} else {
				s.NoError(err)
				s.NotNil(info)
				s.Equal(tt.key, info.Key)
				s.Equal(int64(len(testData)), info.Size)
				s.False(info.Modified.IsZero())
			}
		})
	}
}

// Test Quota enforcement
func (s *FilesystemStorageTestSuite) TestQuotaEnforcement() {
	ctx := context.Background()

	// Create a new filesystem with small quota (100 bytes)
	smallQuotaDir, err := os.MkdirTemp("", "gohoarder-quota-*")
	s.Require().NoError(err)
	defer os.RemoveAll(smallQuotaDir)

	smallFs, err := New(smallQuotaDir, 100)
	s.Require().NoError(err)
	defer smallFs.Close() // #nosec G104 -- Cleanup, error not critical

	// First write should succeed
	err = smallFs.Put(ctx, "file1.txt", strings.NewReader("small content"), nil)
	s.NoError(err)

	// Large write should fail due to quota
	largeData := strings.Repeat("x", 200)
	err = smallFs.Put(ctx, "large.txt", strings.NewReader(largeData), nil)
	s.Error(err)

	// Verify quota info
	quotaInfo, err := smallFs.GetQuota(ctx)
	s.NoError(err)
	s.Equal(int64(100), quotaInfo.Limit)
	s.Greater(quotaInfo.Used, int64(0))
	s.LessOrEqual(quotaInfo.Used, quotaInfo.Limit)
}

// Test GetQuota operation
func (s *FilesystemStorageTestSuite) TestGetQuota() {
	ctx := context.Background()

	// Put some files
	err := s.fs.Put(ctx, "file1.txt", strings.NewReader("content1"), nil)
	s.Require().NoError(err)
	err = s.fs.Put(ctx, "file2.txt", strings.NewReader("content2"), nil)
	s.Require().NoError(err)

	quotaInfo, err := s.fs.GetQuota(ctx)
	s.NoError(err)
	s.NotNil(quotaInfo)
	s.Equal(int64(1024*1024), quotaInfo.Limit)
	s.Greater(quotaInfo.Used, int64(0))
	s.Greater(quotaInfo.Available, int64(0))
	s.Equal(quotaInfo.Limit, quotaInfo.Used+quotaInfo.Available)
}

// Test Health check
func (s *FilesystemStorageTestSuite) TestHealth() {
	ctx := context.Background()

	// Healthy filesystem
	err := s.fs.Health(ctx)
	s.NoError(err)

	// Unhealthy filesystem (removed directory)
	badDir := filepath.Join(s.tempDir, "nonexistent")
	badFs := &FilesystemStorage{basePath: badDir}
	err = badFs.Health(ctx)
	s.Error(err)
}

// Test Context cancellation
func (s *FilesystemStorageTestSuite) TestContextCancellation() {
	// Create cancelled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	tests := []struct {
		fn   func() error
		name string
	}{
		{
			name: "Get with cancelled context",
			fn: func() error {
				_, err := s.fs.Get(ctx, "test.txt")
				return err
			},
		},
		{
			name: "Put with cancelled context",
			fn: func() error {
				return s.fs.Put(ctx, "test.txt", strings.NewReader("data"), nil)
			},
		},
		{
			name: "Delete with cancelled context",
			fn: func() error {
				return s.fs.Delete(ctx, "test.txt")
			},
		},
		{
			name: "Exists with cancelled context",
			fn: func() error {
				_, err := s.fs.Exists(ctx, "test.txt")
				return err
			},
		},
		{
			name: "List with cancelled context",
			fn: func() error {
				_, err := s.fs.List(ctx, "test", nil)
				return err
			},
		},
		{
			name: "Stat with cancelled context",
			fn: func() error {
				_, err := s.fs.Stat(ctx, "test.txt")
				return err
			},
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			err := tt.fn()
			s.Error(err)
			s.Equal(context.Canceled, err)
		})
	}
}

// Test concurrent access (race condition testing)
func (s *FilesystemStorageTestSuite) TestConcurrentAccess() {
	ctx := context.Background()
	numGoroutines := 10
	numOperations := 100

	var wg sync.WaitGroup

	// Concurrent writes
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < numOperations; j++ {
				key := fmt.Sprintf("concurrent/%d/%d.txt", id, j)
				data := fmt.Sprintf("data-%d-%d", id, j)
				err := s.fs.Put(ctx, key, strings.NewReader(data), nil)
				s.NoError(err)
			}
		}(i)
	}

	wg.Wait()

	// Verify all files exist
	objects, err := s.fs.List(ctx, "concurrent", nil)
	s.NoError(err)
	s.Equal(numGoroutines*numOperations, len(objects))
}

// Test concurrent reads and writes
func (s *FilesystemStorageTestSuite) TestConcurrentReadsAndWrites() {
	ctx := context.Background()

	// Setup: Create some initial files
	for i := 0; i < 10; i++ {
		key := fmt.Sprintf("shared/file-%d.txt", i)
		err := s.fs.Put(ctx, key, strings.NewReader(fmt.Sprintf("initial-%d", i)), nil)
		s.Require().NoError(err)
	}

	var wg sync.WaitGroup
	numReaders := 5
	numWriters := 5
	numOps := 50

	// Concurrent readers
	for i := 0; i < numReaders; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < numOps; j++ {
				key := fmt.Sprintf("shared/file-%d.txt", j%10)
				reader, err := s.fs.Get(ctx, key)
				if err == nil {
					_, _ = io.ReadAll(reader)
					_ = reader.Close() // #nosec G104 -- Cleanup, error not critical
				}
			}
		}(i)
	}

	// Concurrent writers
	for i := 0; i < numWriters; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < numOps; j++ {
				key := fmt.Sprintf("shared/writer-%d-%d.txt", id, j)
				data := fmt.Sprintf("writer-%d-%d", id, j)
				_ = s.fs.Put(ctx, key, strings.NewReader(data), nil)
			}
		}(i)
	}

	wg.Wait()

	// Verify quota tracking is consistent
	quotaInfo, err := s.fs.GetQuota(ctx)
	s.NoError(err)
	s.Greater(quotaInfo.Used, int64(0))
}

// Test Delete updates quota correctly
func (s *FilesystemStorageTestSuite) TestDeleteUpdatesQuota() {
	ctx := context.Background()

	// Put a file
	testData := "test data for quota tracking"
	err := s.fs.Put(ctx, "quota/test.txt", strings.NewReader(testData), nil)
	s.Require().NoError(err)

	// Get quota before delete
	quotaBefore, err := s.fs.GetQuota(ctx)
	s.Require().NoError(err)

	// Delete the file
	err = s.fs.Delete(ctx, "quota/test.txt")
	s.NoError(err)

	// Get quota after delete
	quotaAfter, err := s.fs.GetQuota(ctx)
	s.NoError(err)

	// Quota should have decreased
	s.Less(quotaAfter.Used, quotaBefore.Used)
}

// Test atomic write behavior
func (s *FilesystemStorageTestSuite) TestAtomicWrite() {
	ctx := context.Background()
	key := "atomic/test.txt"

	// Initial write
	err := s.fs.Put(ctx, key, strings.NewReader("initial"), nil)
	s.Require().NoError(err)

	// Concurrent readers should never see partial writes
	var wg sync.WaitGroup
	stopReading := make(chan struct{})
	readErrors := make(chan error, 100)

	// Start readers
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for {
				select {
				case <-stopReading:
					return
				default:
					reader, getErr := s.fs.Get(ctx, key)
					if getErr != nil {
						readErrors <- getErr
						continue
					}
					data, readErr := io.ReadAll(reader)
					reader.Close() // #nosec G104 -- Cleanup, error not critical
					if readErr != nil {
						readErrors <- readErr
						continue
					}
					// Data should be either "initial" or "updated", never partial
					content := string(data)
					if content != "initial" && content != "updated" {
						readErrors <- fmt.Errorf("read partial data: %s", content)
					}
				}
			}
		}()
	}

	// Perform update
	time.Sleep(10 * time.Millisecond)
	err = s.fs.Put(ctx, key, strings.NewReader("updated"), nil)
	s.NoError(err)

	// Stop readers
	time.Sleep(10 * time.Millisecond)
	close(stopReading)
	wg.Wait()
	close(readErrors)

	// Check for read errors
	for err := range readErrors {
		s.NoError(err)
	}
}

// Test path sanitization
func (s *FilesystemStorageTestSuite) TestPathSanitization() {
	ctx := context.Background()

	maliciousPaths := []string{
		"../../../etc/passwd",
		"/../secret.txt",
		"./../../outside.txt",
		"//etc/passwd",
	}

	for _, path := range maliciousPaths {
		s.Run(fmt.Sprintf("sanitize_%s", path), func() {
			err := s.fs.Put(ctx, path, strings.NewReader("malicious"), nil)
			s.NoError(err) // Should succeed but sanitize path

			// Verify file is inside base directory
			sanitized, sanitizeErr := s.fs.keyToPath(path)
			s.NoError(sanitizeErr)
			s.True(strings.HasPrefix(sanitized, s.tempDir),
				"Sanitized path %s should be inside %s", sanitized, s.tempDir)
		})
	}
}

// Test checksum validation
func (s *FilesystemStorageTestSuite) TestChecksumValidation() {
	ctx := context.Background()

	testData := "checksum test data"
	// Correct checksums calculated for "checksum test data"
	correctMD5 := "7dd7323e8ce3e087972f93d3711ef62b"

	tests := []struct {
		opts        *storage.PutOptions
		name        string
		expectError bool
	}{
		{
			name:        "valid MD5",
			opts:        &storage.PutOptions{ChecksumMD5: correctMD5},
			expectError: false,
		},
		{
			name:        "invalid MD5",
			opts:        &storage.PutOptions{ChecksumMD5: "invalid"},
			expectError: true,
		},
		{
			name:        "empty checksum (no validation)",
			opts:        &storage.PutOptions{ChecksumMD5: ""},
			expectError: false,
		},
	}

	for _, tt := range tests {
		s.Run(tt.name, func() {
			key := fmt.Sprintf("checksum/%s.txt", tt.name)
			err := s.fs.Put(ctx, key, strings.NewReader(testData), tt.opts)

			if tt.expectError {
				s.Error(err)
			} else {
				s.NoError(err)
			}
		})
	}
}

// Benchmark Put operation
func BenchmarkFilesystemPut(b *testing.B) {
	tempDir, _ := os.MkdirTemp("", "gohoarder-bench-*")
	defer os.RemoveAll(tempDir)

	fs, _ := New(tempDir, 1024*1024*1024) // 1GB quota
	defer fs.Close()                      // #nosec G104 -- Cleanup, error not critical

	ctx := context.Background()
	data := strings.Repeat("x", 1024) // 1KB

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		key := fmt.Sprintf("bench/file-%d.txt", i)
		_ = fs.Put(ctx, key, strings.NewReader(data), nil)
	}
}

// Benchmark Get operation
func BenchmarkFilesystemGet(b *testing.B) {
	tempDir, _ := os.MkdirTemp("", "gohoarder-bench-*")
	defer os.RemoveAll(tempDir)

	fs, _ := New(tempDir, 1024*1024*1024)
	defer fs.Close() // #nosec G104 -- Cleanup, error not critical

	ctx := context.Background()
	data := strings.Repeat("x", 1024)

	// Setup: Create test file
	_ = fs.Put(ctx, "bench/test.txt", strings.NewReader(data), nil)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		reader, _ := fs.Get(ctx, "bench/test.txt")
		if reader != nil {
			_, _ = io.ReadAll(reader)
			_ = reader.Close() // #nosec G104 -- Cleanup, error not critical
		}
	}
}
