package storage

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/server/internal/server/fileserviceimp/storage/config"
	"go.uber.org/zap"
)

// Mock domain provider for testing
type mockDomainProvider struct{}

func (m *mockDomainProvider) GetAPILink(linkElem ...string) (string, error) {
	if len(linkElem) == 0 {
		return "", fmt.Errorf("no link elements provided")
	}
	return "/api/v1/files/" + linkElem[len(linkElem)-1], nil
}

func (m *mockDomainProvider) GetFullLink(linkElem ...string) (string, error) {
	if len(linkElem) == 0 {
		return "", fmt.Errorf("no link elements provided")
	}
	return "https://test.local/api/v1/files/" + linkElem[len(linkElem)-1], nil
}

func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
	// Create temporary directory for testing
	tempDir, err := os.MkdirTemp("", "storage_test")
	require.NoError(t, err)

	// Create logger
	logger := zap.NewNop()

	// Create storage
	storage := &LocalStorage{
		logger:     logger.Named("lfs").Named("test"),
		storageDir: tempDir,
		subDir:     "test",
		directory:  "test",
		dp:         &mockDomainProvider{},
		service:    mservice.Storage,
	}

	// Return cleanup function
	cleanup := func() {
		require.NoError(t, os.RemoveAll(tempDir))
	}

	return storage, tempDir, cleanup
}

func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
	// Create temporary directory for testing
	tempDir, err := os.MkdirTemp("", "storage_bench")
	require.NoError(b, err)

	// Create logger
	logger := zap.NewNop()

	// Create storage
	storage := &LocalStorage{
		logger:     logger.Named("lfs").Named("test"),
		storageDir: tempDir,
		subDir:     "test",
		directory:  "test",
		dp:         &mockDomainProvider{},
		service:    mservice.Storage,
	}

	// Return cleanup function
	cleanup := func() {
		require.NoError(b, os.RemoveAll(tempDir))
	}

	return storage, tempDir, cleanup
}

func TestLocalStorage_Save(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	tests := []struct {
		name    string
		content string
		objID   string
		wantURL string
		wantErr bool
	}{
		{
			name:    "save simple file",
			content: "test content",
			objID:   "test.txt",
			wantURL: "/api/v1/files/test.txt",
			wantErr: false,
		},
		{
			name:    "save with special characters",
			content: "special content",
			objID:   "test-file_123.txt",
			wantURL: "/api/v1/files/test-file_123.txt",
			wantErr: false,
		},
		{
			name:    "save empty file",
			content: "",
			objID:   "empty.txt",
			wantURL: "/api/v1/files/empty.txt",
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			reader := strings.NewReader(tt.content)

			url, err := storage.Save(ctx, reader, tt.objID)

			if tt.wantErr {
				assert.Error(t, err)
				return
			}

			assert.NoError(t, err)
			assert.Equal(t, tt.wantURL, url)

			// Verify file was actually saved
			filePath := filepath.Join(tempDir, tt.objID)
			//nolint:gosec // Test-controlled path inside temporary directory.
			content, err := os.ReadFile(filePath)
			assert.NoError(t, err)
			assert.Equal(t, tt.content, string(content))
		})
	}
}

func TestLocalStorage_Save_ContextCancellation(t *testing.T) {
	storage, _, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a context that's already cancelled
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	reader := strings.NewReader("test content")
	url, err := storage.Save(ctx, reader, "test.txt")

	assert.Error(t, err)
	assert.Equal(t, context.Canceled, err)
	assert.Empty(t, url)
}

func TestLocalStorage_Save_ContextTimeout(t *testing.T) {
	storage, _, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a context with a very short timeout
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
	defer cancel()

	// Wait a bit to ensure timeout
	time.Sleep(1 * time.Millisecond)

	reader := strings.NewReader("test content")
	url, err := storage.Save(ctx, reader, "test.txt")

	assert.Error(t, err)
	assert.Equal(t, context.DeadlineExceeded, err)
	assert.Empty(t, url)
}

func TestLocalStorage_Delete(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a test file
	testFile := filepath.Join(tempDir, "test.txt")
	err := os.WriteFile(testFile, []byte("test content"), 0o600)
	require.NoError(t, err)

	tests := []struct {
		name    string
		objID   string
		wantErr bool
	}{
		{
			name:    "delete existing file",
			objID:   "test.txt",
			wantErr: false,
		},
		{
			name:    "delete non-existent file",
			objID:   "nonexistent.txt",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			err := storage.Delete(ctx, tt.objID)

			if tt.wantErr {
				assert.Error(t, err)
				return
			}

			assert.NoError(t, err)

			// Verify file was actually deleted
			filePath := filepath.Join(tempDir, tt.objID)
			_, err = os.Stat(filePath)
			assert.True(t, os.IsNotExist(err))
		})
	}
}

func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a test file
	testFile := filepath.Join(tempDir, "test.txt")
	err := os.WriteFile(testFile, []byte("test content"), 0o600)
	require.NoError(t, err)

	// Create a context that's already cancelled
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	err = storage.Delete(ctx, "test.txt")

	assert.Error(t, err)
	assert.Equal(t, context.Canceled, err)

	// File should still exist since operation was cancelled
	_, err = os.Stat(testFile)
	assert.NoError(t, err)
}

func TestLocalStorage_Get(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a test file
	testContent := "test file content"
	testFile := filepath.Join(tempDir, "test.txt")
	err := os.WriteFile(testFile, []byte(testContent), 0o600)
	require.NoError(t, err)

	tests := []struct {
		name           string
		objID          string
		wantStatusCode int
		wantContent    string
	}{
		{
			name:           "get existing file",
			objID:          "test.txt",
			wantStatusCode: http.StatusOK,
			wantContent:    testContent,
		},
		{
			name:           "get non-existent file",
			objID:          "nonexistent.txt",
			wantStatusCode: http.StatusInternalServerError,
			wantContent:    "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			handler := storage.Get(ctx, tt.objID)

			// Create test request
			req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
			w := httptest.NewRecorder()

			handler.ServeHTTP(w, req)

			assert.Equal(t, tt.wantStatusCode, w.Code)
			if tt.wantContent != "" {
				assert.Equal(t, tt.wantContent, w.Body.String())
			}
		})
	}
}

func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a test file
	testFile := filepath.Join(tempDir, "test.txt")
	err := os.WriteFile(testFile, []byte("test content"), 0o600)
	require.NoError(t, err)

	// Create a context that's already cancelled
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	handler := storage.Get(ctx, "test.txt")

	// Create test request
	req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
	w := httptest.NewRecorder()

	handler.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)
}

func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a test file
	testFile := filepath.Join(tempDir, "test.txt")
	err := os.WriteFile(testFile, []byte("test content"), 0o600)
	require.NoError(t, err)

	ctx := context.Background()
	handler := storage.Get(ctx, "test.txt")

	// Create test request with cancelled context
	req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
	reqCtx, cancel := context.WithCancel(req.Context())
	req = req.WithContext(reqCtx)
	cancel() // Cancel the request context

	w := httptest.NewRecorder()

	handler.ServeHTTP(w, req)

	assert.Equal(t, http.StatusRequestTimeout, w.Code)
	assert.Contains(t, w.Body.String(), "Request cancelled")
}

func TestCreateLocalFileStorage(t *testing.T) {
	// Create temporary directory for testing
	tempDir, err := os.MkdirTemp("", "storage_test")
	require.NoError(t, err)
	defer func() {
		require.NoError(t, os.RemoveAll(tempDir))
	}()

	logger := zap.NewNop()
	cfg := config.LocalFSSConfig{
		RootPath: tempDir,
	}

	storage, err := CreateLocalFileStorage(logger, mservice.Storage, "test", "sub", &mockDomainProvider{}, cfg)

	assert.NoError(t, err)
	assert.NotNil(t, storage)
	assert.Equal(t, filepath.Join(tempDir, "test"), storage.storageDir)
	assert.Equal(t, "test", storage.directory)
	assert.Equal(t, "sub", storage.subDir)
}

func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
	// Build a deterministic failure case: the target path already exists as a file.
	tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
	require.NoError(t, err)
	defer func() {
		require.NoError(t, os.RemoveAll(tempDir))
	}()

	fileAtTargetPath := filepath.Join(tempDir, "test")
	err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
	require.NoError(t, err)

	logger := zap.NewNop()
	cfg := config.LocalFSSConfig{
		RootPath: tempDir,
	}

	storage, err := CreateLocalFileStorage(logger, mservice.Storage, "test", "sub", &mockDomainProvider{}, cfg)

	assert.Error(t, err)
	assert.Nil(t, storage)
}

func TestLocalStorage_ConcurrentOperations(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Test concurrent saves
	t.Run("concurrent saves", func(t *testing.T) {
		const numGoroutines = 10
		errCh := make(chan error, numGoroutines)

		for i := 0; i < numGoroutines; i++ {
			go func(id int) {
				ctx := context.Background()
				content := strings.NewReader(fmt.Sprintf("content %d", id))
				_, err := storage.Save(ctx, content, fmt.Sprintf("file_%d.txt", id))
				errCh <- err
			}(i)
		}

		// Collect results
		for i := 0; i < numGoroutines; i++ {
			err := <-errCh
			assert.NoError(t, err)
		}

		// Verify all files were created
		for i := 0; i < numGoroutines; i++ {
			filePath := filepath.Join(tempDir, fmt.Sprintf("file_%d.txt", i))
			_, err := os.Stat(filePath)
			assert.NoError(t, err)
		}
	})

	// Test concurrent deletes
	t.Run("concurrent deletes", func(t *testing.T) {
		// Create files to delete
		for i := 0; i < 5; i++ {
			filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
			err := os.WriteFile(filePath, []byte("content"), 0o600)
			require.NoError(t, err)
		}

		const numGoroutines = 5
		errCh := make(chan error, numGoroutines)

		for i := 0; i < numGoroutines; i++ {
			go func(id int) {
				ctx := context.Background()
				err := storage.Delete(ctx, fmt.Sprintf("delete_%d.txt", id))
				errCh <- err
			}(i)
		}

		// Collect results
		for i := 0; i < numGoroutines; i++ {
			err := <-errCh
			assert.NoError(t, err)
		}

		// Verify all files were deleted
		for i := 0; i < numGoroutines; i++ {
			filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
			_, err := os.Stat(filePath)
			assert.True(t, os.IsNotExist(err))
		}
	})
}

func TestLocalStorage_LargeFile(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Create a large content (1MB)
	largeContent := strings.Repeat("a", 1024*1024)
	reader := strings.NewReader(largeContent)

	ctx := context.Background()
	url, err := storage.Save(ctx, reader, "large.txt")

	assert.NoError(t, err)
	assert.Equal(t, "/api/v1/files/large.txt", url)

	// Verify file size
	filePath := filepath.Join(tempDir, "large.txt")
	info, err := os.Stat(filePath)
	assert.NoError(t, err)
	assert.Equal(t, int64(1024*1024), info.Size())
}

func TestLocalStorage_SpecialCharacters(t *testing.T) {
	storage, tempDir, cleanup := setupTestStorage(t)
	defer cleanup()

	// Test with special characters in filename
	specialNames := []string{
		"file with spaces.txt",
		"file-with-dashes.txt",
		"file_with_underscores.txt",
		"file.with.dots.txt",
		"file(1).txt",
		"file[1].txt",
		"file{1}.txt",
		"file@#$%.txt",
	}

	for _, name := range specialNames {
		t.Run("special characters: "+name, func(t *testing.T) {
			ctx := context.Background()
			content := strings.NewReader("test content")
			url, err := storage.Save(ctx, content, name)

			assert.NoError(t, err)
			assert.Equal(t, "/api/v1/files/"+name, url)

			// Verify file exists
			filePath := filepath.Join(tempDir, name)
			_, err = os.Stat(filePath)
			assert.NoError(t, err)
		})
	}
}

// Benchmark tests
func BenchmarkLocalStorage_Save(b *testing.B) {
	storage, _, cleanup := setupBenchmarkStorage(b)
	defer cleanup()

	content := strings.Repeat("test content ", 1000) // ~13KB
	reader := strings.NewReader(content)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ctx := context.Background()
		_, err := storage.Save(ctx, reader, fmt.Sprintf("bench_%d.txt", i))
		if err != nil {
			b.Fatal(err)
		}
		reader.Reset(content)
	}
}

func BenchmarkLocalStorage_Delete(b *testing.B) {
	storage, tempDir, cleanup := setupBenchmarkStorage(b)
	defer cleanup()

	// Pre-create files for deletion
	for i := 0; i < b.N; i++ {
		filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
		err := os.WriteFile(filePath, []byte("content"), 0o600)
		if err != nil {
			b.Fatal(err)
		}
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ctx := context.Background()
		err := storage.Delete(ctx, fmt.Sprintf("bench_delete_%d.txt", i))
		if err != nil {
			b.Fatal(err)
		}
	}
}
