package tests

import (
	"bufio"
	"context"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"sync"
	"syscall"
	"testing"
	"time"

	"github.com/shirou/gopsutil/process"
)

type TestSummary struct {
	Passed              []string
	Failed              []string
	CPUUsage            []float64
	RAMUsage            []uint64
	CacheTests          []CacheTestResult
	ConcurrentTestStats ConcurrentTestStats
}

type CacheTestResult struct {
	SearchType       string
	InitialDuration  time.Duration
	CachedDuration   time.Duration
	IsCacheEffective bool
}

type ConcurrentTestStats struct {
	TotalRequests   int
	TotalFailures   int
	RequestsPerType map[string]int
	FailuresPerType map[string]int
}

func TestApplication(t *testing.T) {
	// Initialize test summary
	summary := &TestSummary{
		ConcurrentTestStats: ConcurrentTestStats{
			RequestsPerType: make(map[string]int),
			FailuresPerType: make(map[string]int),
		},
	}

	// Ensure the test runs from the root directory
	rootDir := "../" // Path to the root directory of the repository

	// Build the application using `run.sh --build`
	buildCmd := exec.Command("sh", "./run.sh", "--build")
	buildCmd.Dir = rootDir

	buildOutput, err := buildCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("Failed to build application: %v\nOutput:\n%s", err, string(buildOutput))
	}
	t.Log("Application built successfully")

	// Path to the built executable relative to rootDir
	executablePath := "./qgato" // Since cmd.Dir is rootDir, this path is relative to rootDir

	// Ensure the executable has execute permissions
	execFullPath := filepath.Join(rootDir, "qgato")
	if err := os.Chmod(execFullPath, 0755); err != nil {
		t.Fatalf("Failed to set execute permissions on the executable: %v", err)
	}

	// Create a context with cancellation
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // Ensure resources are cleaned up

	// Start the application using the built executable
	cmd := exec.CommandContext(ctx, executablePath, "--skip-config-check")
	cmd.Dir = rootDir // Set the working directory to the root directory

	// Set process group ID so we can kill it and its children
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: true,
	}

	// Capture application output for logging
	appStdout, err := cmd.StdoutPipe()
	if err != nil {
		t.Fatalf("Failed to capture stdout: %v", err)
	}
	appStderr, err := cmd.StderrPipe()
	if err != nil {
		t.Fatalf("Failed to capture stderr: %v", err)
	}

	// Start the application
	if err := cmd.Start(); err != nil {
		t.Fatalf("Failed to start application: %v", err)
	}

	// Read application logs concurrently
	go func() {
		scanner := bufio.NewScanner(appStdout)
		for scanner.Scan() {
			t.Logf("[APP STDOUT] %s", scanner.Text())
		}
	}()
	go func() {
		scanner := bufio.NewScanner(appStderr)
		for scanner.Scan() {
			t.Logf("[APP STDERR] %s", scanner.Text())
		}
	}()

	// Defer cleanup to ensure process is killed after the test
	defer func() {
		// Kill the process group
		pgid, err := syscall.Getpgid(cmd.Process.Pid)
		if err == nil {
			syscall.Kill(-pgid, syscall.SIGKILL)
		} else {
			t.Logf("Failed to get process group ID: %v", err)
			cmd.Process.Kill()
		}
		cmd.Wait()

		// Print summary
		printSummary(summary, t)
	}()

	// Wait for the server to start
	if !waitForServer("http://localhost:5000", 15*time.Second) {
		t.Fatalf("Server did not start within the expected time")
	}

	t.Log("Application is running")

	// Create a process instance for the application
	appProcess, err := process.NewProcess(int32(cmd.Process.Pid))
	if err != nil {
		t.Fatalf("Failed to create process instance: %v", err)
	}

	// Run test functions and collect results
	runTest(t, summary, "Check Idle Resource Usage", func(t *testing.T) {
		checkResourceUsage(t, summary, appProcess)
	})
	runTest(t, summary, "Test Endpoints", testEndpoints)
	runTest(t, summary, "Test Search Types and Cache Effectiveness", func(t *testing.T) {
		testSearchTypesAndCache(t, summary)
	})
	runTest(t, summary, "Test Concurrent Random Requests", func(t *testing.T) {
		testConcurrentRandomRequests(t, summary)
	})
	runTest(t, summary, "Test Suggestions API", testSuggestionsAPI)
	runTest(t, summary, "Check Resource Usage After Tests", func(t *testing.T) {
		checkResourceUsage(t, summary, appProcess)
	})
}

func runTest(t *testing.T, summary *TestSummary, name string, testFunc func(t *testing.T)) {
	t.Run(name, func(t *testing.T) {
		defer func() {
			if r := recover(); r != nil {
				t.Errorf("Test '%s' panicked: %v", name, r)
				summary.Failed = append(summary.Failed, name)
			}
		}()
		testFunc(t)
		if !t.Failed() {
			summary.Passed = append(summary.Passed, name)
		} else {
			summary.Failed = append(summary.Failed, name)
		}
	})
}

func printSummary(summary *TestSummary, t *testing.T) {
	t.Logf("\n==== TEST SUMMARY ====")
	t.Logf("PASSED TESTS: %d", len(summary.Passed))
	for _, test := range summary.Passed {
		t.Logf("  - %s", test)
	}
	t.Logf("FAILED TESTS: %d", len(summary.Failed))
	for _, test := range summary.Failed {
		t.Logf("  - %s", test)
	}
	t.Logf("\nResource Usage:")
	for i, cpu := range summary.CPUUsage {
		t.Logf("  CPU Usage Sample %d: %.2f%%", i+1, cpu)
	}
	for i, ram := range summary.RAMUsage {
		t.Logf("  RAM Usage Sample %d: %d MiB", i+1, ram)
	}
	t.Logf("\nCache Test Results:")
	for _, result := range summary.CacheTests {
		t.Logf("  Search Type: %s", result.SearchType)
		t.Logf("    Initial Duration: %v", result.InitialDuration)
		t.Logf("    Cached Duration: %v", result.CachedDuration)
		if result.IsCacheEffective {
			t.Logf("    Cache Effective: Yes")
		} else {
			t.Logf("    Cache Effective: No")
		}
	}

	t.Logf("\nConcurrent Random Requests Test Stats:")
	t.Logf("  Total Requests Sent: %d", summary.ConcurrentTestStats.TotalRequests)
	t.Logf("  Total Failures: %d", summary.ConcurrentTestStats.TotalFailures)
	for stype, reqCount := range summary.ConcurrentTestStats.RequestsPerType {
		failCount := summary.ConcurrentTestStats.FailuresPerType[stype]
		t.Logf("    Type '%s': %d requests, %d failures", stype, reqCount, failCount)
	}
	t.Logf("\n======================\n")
}

func checkResourceUsage(t *testing.T, summary *TestSummary, appProcess *process.Process) {
	// Get CPU usage of the application process
	cpuPercent, err := appProcess.Percent(time.Second)
	if err != nil {
		t.Errorf("Failed to get CPU usage: %v", err)
		summary.CPUUsage = append(summary.CPUUsage, 0.0)
	} else {
		// Sum the CPU usage of the main process and its children
		totalCPU := cpuPercent
		children, err := appProcess.Children()
		if err != nil {
			t.Logf("Failed to get child processes: %v", err)
		} else {
			for _, child := range children {
				childCPU, err := child.Percent(0)
				if err != nil {
					t.Logf("Failed to get CPU usage for child process %d: %v", child.Pid, err)
					continue
				}
				totalCPU += childCPU
			}
		}
		summary.CPUUsage = append(summary.CPUUsage, totalCPU)
		t.Logf("Total CPU Usage (process and children): %.2f%%", totalCPU)
	}

	// Get memory info of the application process
	memInfo, err := appProcess.MemoryInfo()
	if err != nil {
		t.Errorf("Failed to get memory info: %v", err)
		summary.RAMUsage = append(summary.RAMUsage, 0)
	} else {
		totalRAM := memInfo.RSS
		children, err := appProcess.Children()
		if err != nil {
			t.Logf("Failed to get child processes: %v", err)
		} else {
			for _, child := range children {
				childMemInfo, err := child.MemoryInfo()
				if err != nil {
					t.Logf("Failed to get memory info for child process %d: %v", child.Pid, err)
					continue
				}
				totalRAM += childMemInfo.RSS
			}
		}
		ramUsage := bToMb(totalRAM)
		summary.RAMUsage = append(summary.RAMUsage, ramUsage)
		t.Logf("Total Memory Usage (process and children): %d MiB", ramUsage)
	}
}

func waitForServer(url string, timeout time.Duration) bool {
	start := time.Now()
	for {
		if time.Since(start) > timeout {
			return false
		}
		resp, err := http.Get(url)
		if err == nil && resp.StatusCode == http.StatusOK {
			return true
		}
		time.Sleep(500 * time.Millisecond)
	}
}

func testEndpoints(t *testing.T) {
	endpoints := []string{
		"/",
		"/settings",
	}
	for _, endpoint := range endpoints {
		resp, err := http.Get("http://localhost:5000" + endpoint)
		if err != nil {
			t.Errorf("Failed to GET %s: %v", endpoint, err)
			continue
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("%s returned status code %d", endpoint, resp.StatusCode)
		}
	}
}

func testSearchTypesAndCache(t *testing.T, summary *TestSummary) {
	searchTypes := []string{"text", "image", "video", "forum", "map", "file"}
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	for _, searchType := range searchTypes {
		url := "http://localhost:5000/search?q=test&t=" + searchType

		// Initial search
		start := time.Now()
		resp, err := client.Get(url)
		if err != nil {
			t.Errorf("Failed to GET %s: %v", url, err)
			continue
		}
		initialDuration := time.Since(start)
		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			t.Errorf("Failed to read response body for %s: %v", url, err)
			continue
		}
		if resp.StatusCode != http.StatusOK {
			t.Errorf("Search type '%s' returned status code %d", searchType, resp.StatusCode)
		}
		if len(body) == 0 {
			t.Errorf("Response body for %s is empty", url)
		}
		t.Logf("Search type '%s' took %v", searchType, initialDuration)

		// If initial search took longer than 10 seconds, skip
		if initialDuration > 10*time.Second {
			t.Logf("Search type '%s' took too long (%v), skipping cached search", searchType, initialDuration)
			continue
		}

		// Cached search
		time.Sleep(1 * time.Second)
		start = time.Now()
		resp, err = client.Get(url)
		if err != nil {
			t.Errorf("Failed to GET %s (cached): %v", url, err)
			continue
		}
		cachedDuration := time.Since(start)
		body, err = io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			t.Errorf("Failed to read response body for cached %s: %v", url, err)
			continue
		}
		if resp.StatusCode != http.StatusOK {
			t.Errorf("Cached search type '%s' returned status code %d", searchType, resp.StatusCode)
		}
		if len(body) == 0 {
			t.Errorf("Response body for cached %s is empty", url)
		}
		t.Logf("Cached search type '%s' took %v", searchType, cachedDuration)

		// Check if cache was effective
		isCacheEffective := cachedDuration < initialDuration
		if !isCacheEffective {
			t.Errorf("Cache not effective for search type '%s'", searchType)
		}

		summary.CacheTests = append(summary.CacheTests, CacheTestResult{
			SearchType:       searchType,
			InitialDuration:  initialDuration,
			CachedDuration:   cachedDuration,
			IsCacheEffective: isCacheEffective,
		})
	}
}

func testSuggestionsAPI(t *testing.T) {
	url := "http://localhost:5000/suggestions?q=test"
	resp, err := http.Get(url)
	if err != nil {
		t.Errorf("Failed to GET %s: %v", url, err)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Errorf("Suggestions API returned status code %d", resp.StatusCode)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Errorf("Failed to read response body for suggestions API: %v", err)
		return
	}
	var data []interface{}
	if err := json.Unmarshal(body, &data); err != nil {
		t.Errorf("Failed to parse JSON response: %v", err)
		t.Logf("Response body: %s", string(body))
	}
}

func bToMb(b uint64) uint64 {
	return b / 1024 / 1024
}

func testConcurrentRandomRequests(t *testing.T, summary *TestSummary) {
	searchTypes := []string{"text", "image", "video", "forum", "map", "file"}
	numRequests := 10 // Number of requests per search type
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	var wg sync.WaitGroup

	for _, stype := range searchTypes {
		summary.ConcurrentTestStats.RequestsPerType[stype] = numRequests
		for i := 0; i < numRequests; i++ {
			wg.Add(1)
			go func(searchType string) {
				defer wg.Done()
				query := getRandomUserQuery(searchType)
				fullURL := fmt.Sprintf("http://localhost:5000/search?q=%s&t=%s", url.QueryEscape(query), searchType)
				resp, err := client.Get(fullURL)
				if err != nil {
					t.Errorf("Failed to GET %s: %v", fullURL, err)
					summary.ConcurrentTestStats.TotalFailures++
					summary.ConcurrentTestStats.FailuresPerType[searchType]++
					return
				}
				defer resp.Body.Close()
				if resp.StatusCode != http.StatusOK {
					t.Errorf("Search type '%s' with query '%s' returned status code %d", searchType, query, resp.StatusCode)
					summary.ConcurrentTestStats.TotalFailures++
					summary.ConcurrentTestStats.FailuresPerType[searchType]++
				}
			}(stype)
		}
	}

	// Wait for all requests to complete
	wg.Wait()

	summary.ConcurrentTestStats.TotalRequests = numRequests * len(searchTypes)
	t.Logf("Concurrent random requests test completed: %d requests sent", summary.ConcurrentTestStats.TotalRequests)
	if summary.ConcurrentTestStats.TotalFailures > 0 {
		t.Errorf("Number of failed requests: %d", summary.ConcurrentTestStats.TotalFailures)
	}
}

// getRandomUserQuery returns a random user-like search query based on the search type
func getRandomUserQuery(searchType string) string {
	var queries []string
	switch searchType {
	case "text":
		queries = []string{
			"weather forecast", "latest tech news", "open source software", "how to cook pasta",
			"learn golang", "famous quotes", "best laptops 2024", "history of linux",
			"simple bread recipe", "mountain climbing tips",
		}
	case "image":
		queries = []string{
			"cute cat pictures", "beautiful landscapes", "famous paintings", "colorful birds",
			"space wallpapers", "vintage cars", "r/unixporn", "minimalist backgrounds",
		}
	case "video":
		queries = []string{
			"cute cats", "music videos", "sports highlights", "coding tutorials",
			"documentaries about space", "stand-up comedy clips", "top movie trailers",
		}
	case "forum":
		queries = []string{
			"linux help forum", "DIY electronics discussion", "programming Q&A",
			"travel tips community", "best gaming computers", "homebrewing advice",
			"gardening support", "car maintenance forum", "best homelab setup",
		}
	case "map":
		queries = []string{
			"coffee shops in New York", "best pizza places in Chicago", "tourist attractions Tokyo",
			"Brazil", "public parks in Berlin", "bookstores in London", "Japan",
		}
	case "file":
		queries = []string{
			"debian iso", "free ebooks", "open source fonts", "linux distribution torrents",
			"arch iso", "alpine linux",
		}
	default:
		// fallback if unknown type
		queries = []string{"test query", "random search", "hello world"}
	}

	// Pick a random query
	n, err := rand.Int(rand.Reader, big.NewInt(int64(len(queries))))
	if err != nil {
		return queries[0] // fallback to first if error occurs
	}
	return queries[n.Int64()]
}