package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"torrentGateway/internal/api"
	"torrentGateway/internal/config"
	"torrentGateway/internal/storage"
	"github.com/gorilla/mux"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestEnvironment represents a test environment
type TestEnvironment struct {
	server     *httptest.Server
	storage    *storage.Backend
	config     *config.Config
	tempDir    string
	t          *testing.T
	testPubkey string
}

// NewTestEnvironment creates a new test environment
func NewTestEnvironment(t *testing.T) *TestEnvironment {
	// Create temporary directory
	tempDir, err := os.MkdirTemp("", "gateway_test_*")
	require.NoError(t, err)

	// Create test config
	cfg := &config.Config{
		Mode: "unified",
		Gateway: config.GatewayConfig{
			Enabled:       true,
			Port:          0, // Will be set by httptest
			MaxUploadSize: "10GB",
		},
		Storage: config.StorageConfig{
			MetadataDB:    filepath.Join(tempDir, "test.db"),
			BlobStorage:   filepath.Join(tempDir, "blobs"),
			ChunkStorage:  filepath.Join(tempDir, "chunks"),
			ChunkSize:     2 * 1024 * 1024, // 2MB
		},
		Admin: config.AdminConfig{
			Enabled: true,
			Pubkeys: []string{"test_admin_pubkey"},
		},
	}

	// Create storage backend
	storageBackend, err := storage.NewBackend(
		cfg.Storage.MetadataDB,
		cfg.Storage.ChunkStorage,
		cfg.Storage.BlobStorage,
		int64(cfg.Storage.ChunkSize),
		cfg,
	)
	require.NoError(t, err)

	// Create test pubkey and session
	testPubkey := "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
	
	// Create router and register routes
	router := mux.NewRouter()
	api.RegisterRoutes(router.PathPrefix("/api").Subrouter(), cfg, storageBackend)
	
	// Create test user and session in database
	db := storageBackend.GetDB()
	_, err = db.Exec(`
		INSERT OR IGNORE INTO users (pubkey, storage_used, file_count, last_login, created_at)
		VALUES (?, 0, 0, ?, ?)
	`, testPubkey, time.Now(), time.Now())
	require.NoError(t, err)
	
	// Create a test session
	sessionToken := "test_session_token_" + testPubkey
	_, err = db.Exec(`
		INSERT OR IGNORE INTO sessions (token, pubkey, created_at, expires_at)
		VALUES (?, ?, ?, ?)
	`, sessionToken, testPubkey, time.Now(), time.Now().Add(24*time.Hour))
	require.NoError(t, err)

	// Create test server
	server := httptest.NewServer(router)

	return &TestEnvironment{
		server:     server,
		storage:    storageBackend,
		config:     cfg,
		tempDir:    tempDir,
		t:          t,
		testPubkey: testPubkey,
	}
}

// Cleanup cleans up test resources
func (te *TestEnvironment) Cleanup() {
	te.server.Close()
	te.storage.Close()
	os.RemoveAll(te.tempDir)
}

// TestFullUploadDownloadCycle tests the complete upload->store->download flow
func TestFullUploadDownloadCycle(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Test data
	testContent := []byte("This is test file content for integration testing")
	filename := "test.txt"

	// Test small file (blob storage)
	t.Run("SmallFileBlob", func(t *testing.T) {
		// Upload file
		uploadResp := uploadTestFile(t, env, testContent, filename)
		assert.NotEmpty(t, uploadResp.FileHash)
		assert.Equal(t, "blob", uploadResp.StorageType)

		// Download file
		downloadedContent := downloadTestFile(t, env, uploadResp.FileHash)
		assert.Equal(t, testContent, downloadedContent)
	})

	// Test large file (torrent storage)
	t.Run("LargeFileTorrent", func(t *testing.T) {
		// Create large test content (>100MB)
		largeContent := make([]byte, 110*1024*1024) // 110MB
		for i := range largeContent {
			largeContent[i] = byte(i % 256)
		}

		// Upload large file
		uploadResp := uploadTestFile(t, env, largeContent, "large_test.bin")
		assert.NotEmpty(t, uploadResp.FileHash)
		assert.Equal(t, "torrent", uploadResp.StorageType)

		// Download large file
		downloadedContent := downloadTestFile(t, env, uploadResp.FileHash)
		assert.Equal(t, largeContent, downloadedContent)
	})
}

// TestAuthenticationFlow tests the complete authentication flow
func TestAuthenticationFlow(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	t.Run("ChallengeGeneration", func(t *testing.T) {
		resp, err := http.Get(env.server.URL + "/api/auth/challenge")
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.Equal(t, http.StatusOK, resp.StatusCode)

		var challengeResp map[string]string
		err = json.NewDecoder(resp.Body).Decode(&challengeResp)
		require.NoError(t, err)

		assert.NotEmpty(t, challengeResp["challenge"])
	})

	t.Run("ProtectedEndpointWithoutAuth", func(t *testing.T) {
		resp, err := http.Get(env.server.URL + "/api/users/me/files")
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})
}

// TestAdminOperations tests admin functionality
func TestAdminOperations(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	t.Run("AdminStats", func(t *testing.T) {
		// Note: This would need mock admin authentication
		// For now, test that the endpoint exists
		resp, err := http.Get(env.server.URL + "/api/admin/stats")
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should return 401 without auth
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})
}

// TestConcurrentUploads tests concurrent upload handling
func TestConcurrentUploads(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	concurrency := 10
	var wg sync.WaitGroup
	results := make(chan UploadResponse, concurrency)
	errors := make(chan error, concurrency)

	// Launch concurrent uploads
	for i := 0; i < concurrency; i++ {
		wg.Add(1)
		go func(index int) {
			defer wg.Done()

			content := []byte(fmt.Sprintf("Test content for file %d", index))
			filename := fmt.Sprintf("test_%d.txt", index)

			resp := uploadTestFile(t, env, content, filename)
			if resp.FileHash != "" {
				results <- resp
			} else {
				errors <- fmt.Errorf("upload %d failed", index)
			}
		}(i)
	}

	// Wait for all uploads to complete
	wg.Wait()
	close(results)
	close(errors)

	// Check results
	successCount := len(results)
	errorCount := len(errors)

	assert.Equal(t, concurrency, successCount+errorCount)
	assert.Greater(t, successCount, errorCount, "More uploads should succeed than fail")

	// Verify each uploaded file can be downloaded
	for result := range results {
		content := downloadTestFile(t, env, result.FileHash)
		assert.NotEmpty(t, content)
	}
}

// TestStorageTypeRouting tests that files are routed to correct storage based on size
func TestStorageTypeRouting(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	testCases := []struct {
		name        string
		contentSize int
		expectedType string
	}{
		{"SmallFile", 1024, "blob"},           // 1KB -> blob
		{"MediumFile", 50*1024*1024, "blob"},  // 50MB -> blob
		{"LargeFile", 150*1024*1024, "torrent"}, // 150MB -> torrent
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			content := make([]byte, tc.contentSize)
			for i := range content {
				content[i] = byte(i % 256)
			}

			resp := uploadTestFile(t, env, content, tc.name+".bin")
			assert.Equal(t, tc.expectedType, resp.StorageType)
		})
	}
}

// TestSystemStats tests the system statistics endpoint
func TestSystemStats(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Upload some test files first
	uploadTestFile(t, env, []byte("blob content"), "blob.txt")
	uploadTestFile(t, env, make([]byte, 150*1024*1024), "torrent.bin") // 150MB

	// Get system stats
	resp, err := http.Get(env.server.URL + "/api/stats")
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var stats map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&stats)
	require.NoError(t, err)

	// Verify stats structure
	assert.Contains(t, stats, "gateway")
	assert.Contains(t, stats, "blossom")
	assert.Contains(t, stats, "dht")
	assert.Contains(t, stats, "system")

	// Verify some values
	gateway := stats["gateway"].(map[string]interface{})
	assert.Equal(t, "healthy", gateway["status"])
	assert.Equal(t, float64(9876), gateway["port"])
}

// TestLoadTesting performs basic load testing
func TestLoadTesting(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping load test in short mode")
	}

	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Test parameters
	numUsers := 50
	uploadsPerUser := 2
	concurrency := make(chan struct{}, 10) // Limit to 10 concurrent operations

	var wg sync.WaitGroup
	successCount := int64(0)
	errorCount := int64(0)
	var mu sync.Mutex

	startTime := time.Now()

	// Simulate multiple users uploading files
	for user := 0; user < numUsers; user++ {
		for upload := 0; upload < uploadsPerUser; upload++ {
			wg.Add(1)
			go func(userID, uploadID int) {
				defer wg.Done()
				concurrency <- struct{}{} // Acquire slot
				defer func() { <-concurrency }() // Release slot

				content := []byte(fmt.Sprintf("User %d upload %d content", userID, uploadID))
				filename := fmt.Sprintf("user_%d_file_%d.txt", userID, uploadID)

				resp := uploadTestFile(t, env, content, filename)
				
				mu.Lock()
				if resp.FileHash != "" {
					successCount++
				} else {
					errorCount++
				}
				mu.Unlock()
			}(user, upload)
		}
	}

	wg.Wait()
	duration := time.Since(startTime)

	t.Logf("Load test completed in %v", duration)
	t.Logf("Successful uploads: %d", successCount)
	t.Logf("Failed uploads: %d", errorCount)
	t.Logf("Throughput: %.2f uploads/second", float64(successCount)/duration.Seconds())

	// Assertions
	assert.Greater(t, successCount, int64(0), "Should have some successful uploads")
	assert.Less(t, errorCount, successCount, "Error rate should be less than success rate")
}

// Helper functions

type UploadResponse struct {
	Success     bool   `json:"success"`
	FileHash    string `json:"file_hash"`
	StorageType string `json:"storage_type"`
	Message     string `json:"message"`
}

func uploadTestFile(t *testing.T, env *TestEnvironment, content []byte, filename string) UploadResponse {
	// Create multipart form
	var buf bytes.Buffer
	writer := multipart.NewWriter(&buf)

	part, err := writer.CreateFormFile("file", filename)
	require.NoError(t, err)

	_, err = part.Write(content)
	require.NoError(t, err)

	err = writer.Close()
	require.NoError(t, err)

	// Create request
	req, err := http.NewRequest("POST", env.server.URL+"/api/upload", &buf)
	require.NoError(t, err)

	req.Header.Set("Content-Type", writer.FormDataContentType())
	
	// Add authentication header
	sessionToken := "test_session_token_" + env.testPubkey
	req.Header.Set("Authorization", "Bearer "+sessionToken)

	// Send request
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	require.NoError(t, err)
	defer resp.Body.Close()

	// Parse response
	var uploadResp UploadResponse
	err = json.NewDecoder(resp.Body).Decode(&uploadResp)
	if err != nil {
		// If JSON parsing fails, read the body as text for debugging
		resp.Body.Close()
		req, _ = http.NewRequest("POST", env.server.URL+"/api/upload", bytes.NewReader(buf.Bytes()))
		req.Header.Set("Content-Type", writer.FormDataContentType())
		resp, _ = client.Do(req)
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Logf("Upload response body: %s", string(bodyBytes))
		require.NoError(t, err)
	}

	if !uploadResp.Success {
		t.Logf("Upload failed: %s", uploadResp.Message)
	}

	return uploadResp
}

func downloadTestFile(t *testing.T, env *TestEnvironment, fileHash string) []byte {
	resp, err := http.Get(env.server.URL + "/api/download/" + fileHash)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)

	content, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	return content
}

// TestDatabaseIntegrity tests database consistency
func TestDatabaseIntegrity(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Upload test files
	blobContent := []byte("blob test content")
	torrentContent := make([]byte, 150*1024*1024) // 150MB

	blobResp := uploadTestFile(t, env, blobContent, "blob.txt")
	torrentResp := uploadTestFile(t, env, torrentContent, "torrent.bin")

	require.True(t, blobResp.Success)
	require.True(t, torrentResp.Success)

	// Test database queries directly
	db := env.storage.GetDB()

	// Check files table
	var fileCount int
	err := db.QueryRow("SELECT COUNT(*) FROM files").Scan(&fileCount)
	require.NoError(t, err)
	assert.Equal(t, 2, fileCount)

	// Check blobs table
	var blobCount int
	err = db.QueryRow("SELECT COUNT(*) FROM blobs").Scan(&blobCount)
	require.NoError(t, err)
	assert.Equal(t, 1, blobCount) // Only blob file should be in blobs table

	// Check chunks table
	var chunkCount int
	err = db.QueryRow("SELECT COUNT(*) FROM chunks").Scan(&chunkCount)
	require.NoError(t, err)
	assert.Greater(t, chunkCount, 0) // Torrent file should have chunks

	// Verify file metadata consistency
	blobMeta, err := env.storage.GetFileMetadata(blobResp.FileHash)
	require.NoError(t, err)
	require.NotNil(t, blobMeta)
	assert.Equal(t, "blob", blobMeta.StorageType)
	assert.Equal(t, int64(len(blobContent)), blobMeta.Size)

	torrentMeta, err := env.storage.GetFileMetadata(torrentResp.FileHash)
	require.NoError(t, err)
	require.NotNil(t, torrentMeta)
	assert.Equal(t, "torrent", torrentMeta.StorageType)
	assert.Equal(t, int64(len(torrentContent)), torrentMeta.Size)
}

// TestCacheIntegration tests caching functionality
func TestCacheIntegration(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Upload a file
	content := []byte("cache test content")
	uploadResp := uploadTestFile(t, env, content, "cache_test.txt")
	require.True(t, uploadResp.Success)

	// Download twice to test caching
	start1 := time.Now()
	content1 := downloadTestFile(t, env, uploadResp.FileHash)
	duration1 := time.Since(start1)

	start2 := time.Now()
	content2 := downloadTestFile(t, env, uploadResp.FileHash)
	duration2 := time.Since(start2)

	// Verify content is identical
	assert.Equal(t, content1, content2)
	assert.Equal(t, content, content1)

	// Second request should be faster (cached)
	// Note: In test environment this might not be significant
	t.Logf("First download: %v, Second download: %v", duration1, duration2)
}

// TestStreamingEndpoint tests HLS streaming functionality
func TestStreamingEndpoint(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Upload a video file
	videoContent := make([]byte, 10*1024*1024) // 10MB simulated video
	uploadResp := uploadTestFile(t, env, videoContent, "test_video.mp4")
	require.True(t, uploadResp.Success)

	// Test streaming endpoint
	resp, err := http.Get(env.server.URL + "/api/stream/" + uploadResp.FileHash)
	require.NoError(t, err)
	defer resp.Body.Close()

	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type"))
}

// TestErrorHandling tests various error conditions
func TestErrorHandling(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	testCases := []struct {
		name           string
		endpoint       string
		method         string
		expectedStatus int
	}{
		{"InvalidFileHash", "/api/download/invalid_hash", "GET", http.StatusBadRequest},
		{"NonexistentFile", "/api/download/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "GET", http.StatusNotFound},
		{"InvalidMethod", "/api/upload", "GET", http.StatusMethodNotAllowed},
		{"NonexistentEndpoint", "/api/nonexistent", "GET", http.StatusNotFound},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			req, err := http.NewRequest(tc.method, env.server.URL+tc.endpoint, nil)
			require.NoError(t, err)

			resp, err := http.DefaultClient.Do(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tc.expectedStatus, resp.StatusCode)
		})
	}
}

// TestPerformanceBenchmarks runs performance benchmarks
func TestPerformanceBenchmarks(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping performance benchmarks in short mode")
	}

	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Benchmark small file uploads
	t.Run("BenchmarkSmallUploads", func(t *testing.T) {
		content := make([]byte, 1024) // 1KB
		iterations := 100

		start := time.Now()
		for i := 0; i < iterations; i++ {
			filename := fmt.Sprintf("bench_small_%d.bin", i)
			resp := uploadTestFile(t, env, content, filename)
			assert.True(t, resp.Success)
		}
		duration := time.Since(start)

		throughput := float64(iterations) / duration.Seconds()
		t.Logf("Small file upload throughput: %.2f files/second", throughput)
		assert.Greater(t, throughput, 10.0, "Should achieve >10 small uploads/second")
	})

	// Benchmark downloads
	t.Run("BenchmarkDownloads", func(t *testing.T) {
		// Upload a test file first
		content := make([]byte, 1024*1024) // 1MB
		uploadResp := uploadTestFile(t, env, content, "download_bench.bin")
		require.True(t, uploadResp.Success)

		iterations := 50
		start := time.Now()
		for i := 0; i < iterations; i++ {
			downloadedContent := downloadTestFile(t, env, uploadResp.FileHash)
			assert.Equal(t, len(content), len(downloadedContent))
		}
		duration := time.Since(start)

		throughput := float64(iterations) / duration.Seconds()
		t.Logf("Download throughput: %.2f downloads/second", throughput)
		assert.Greater(t, throughput, 20.0, "Should achieve >20 downloads/second")
	})
}

// TestDatabaseMigrations tests database schema migrations
func TestDatabaseMigrations(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	db := env.storage.GetDB()

	// Test that all required tables exist
	tables := []string{"files", "chunks", "blobs", "users", "sessions", "admin_actions", "banned_users", "content_reports"}
	
	for _, table := range tables {
		var count int
		err := db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&count)
		assert.NoError(t, err, "Table %s should exist and be queryable", table)
	}

	// Test that all required indexes exist
	var indexCount int
	err := db.QueryRow(`
		SELECT COUNT(*) FROM sqlite_master 
		WHERE type = 'index' AND name LIKE 'idx_%'
	`).Scan(&indexCount)
	require.NoError(t, err)
	assert.Greater(t, indexCount, 10, "Should have multiple indexes for performance")
}