package security

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/jfraeys/fetch_ml/internal/fileutil"
)

func TestSecurePathValidator_ValidatePath(t *testing.T) {
	// Create a temporary directory for testing
	tempDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	tests := []struct {
		name    string
		input   string
		wantErr bool
		errMsg  string
	}{
		{
			name:    "valid relative path",
			input:   "subdir/file.txt",
			wantErr: false,
		},
		{
			name:    "valid absolute path within base",
			input:   filepath.Join(tempDir, "file.txt"),
			wantErr: false,
		},
		{
			name:    "path traversal attempt with dots",
			input:   "../etc/passwd",
			wantErr: true,
			errMsg:  "path escapes base directory",
		},
		{
			name:    "three dots path (not traversal - treated as filename)",
			input:   "...//...//etc/passwd",
			wantErr: false, // filepath.Clean treats "..." as filename, not parent dir
		},
		{
			name:    "absolute path outside base",
			input:   "/etc/passwd",
			wantErr: true,
			errMsg:  "path escapes base directory",
		},
		{
			name:    "empty path returns base",
			input:   "",
			wantErr: false,
		},
		{
			name:    "single dot current directory",
			input:   ".",
			wantErr: false,
		},
	}

	// Create subdir for tests that need it
	_ = os.MkdirAll(filepath.Join(tempDir, "subdir"), 0755)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := validator.ValidatePath(tt.input)
			if tt.wantErr {
				if err == nil {
					t.Errorf("ValidatePath() error = nil, wantErr %v", tt.wantErr)
					return
				}
				if tt.errMsg != "" && err.Error()[:len(tt.errMsg)] != tt.errMsg {
					t.Errorf("ValidatePath() error = %v, want %v", err, tt.errMsg)
				}
			} else {
				if err != nil {
					t.Errorf("ValidatePath() unexpected error = %v", err)
					return
				}
				if got == "" {
					t.Errorf("ValidatePath() returned empty path")
				}
			}
		})
	}
}

func TestSecurePathValidator_SymlinkEscape(t *testing.T) {
	// Create temp directories
	tempDir := t.TempDir()
	outsideDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	// Create a file outside the base directory
	outsideFile := filepath.Join(outsideDir, "secret.txt")
	if err := os.WriteFile(outsideFile, []byte("secret"), 0600); err != nil {
		t.Fatalf("Failed to create outside file: %v", err)
	}

	// Create a symlink inside tempDir pointing outside
	symlinkPath := filepath.Join(tempDir, "link")
	if err := os.Symlink(outsideFile, symlinkPath); err != nil {
		t.Fatalf("Failed to create symlink: %v", err)
	}

	// Attempt to access through symlink should fail
	_, err := validator.ValidatePath("link")
	if err == nil {
		t.Errorf("Symlink escape should be blocked: %v", err)
	}
}

func TestSecurePathValidator_BasePathNotSet(t *testing.T) {
	validator := fileutil.NewSecurePathValidator("")
	_, err := validator.ValidatePath("test.txt")
	if err == nil || err.Error() != "base path not set" {
		t.Errorf("Expected 'base path not set' error, got: %v", err)
	}
}

// TestDeeplyNestedTraversal tests path traversal with many parent directory references
func TestSecurePathValidator_DeeplyNestedTraversal(t *testing.T) {
	tempDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	// Create deeply nested traversal attempt
	deepTraversal := "../../../../../../../../../../../../etc/passwd"
	_, err := validator.ValidatePath(deepTraversal)
	if err == nil {
		t.Errorf("Deeply nested traversal should be blocked: %v", err)
	}

	// Also test with mixed valid/traversal paths
	mixedPath := "data/subdir/../../../../../../etc/passwd"
	_, err = validator.ValidatePath(mixedPath)
	if err == nil {
		t.Errorf("Mixed valid/traversal path should be blocked: %v", err)
	}
}

// TestUnicodePathNormalization tests unicode-based path attacks
func TestSecurePathValidator_UnicodePathNormalization(t *testing.T) {
	tempDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	// Test various unicode variants that might bypass normalization
	unicodePaths := []string{
		"..%c0%af..%c0%afetc/passwd", // Overlong UTF-8 encoding
		"..\u2215..\u2215etc/passwd", // Unicode slash (U+2215)
		"..\uff0f..\uff0fetc/passwd", // Fullwidth solidus (U+FF0F)
		"file\x00.txt",               // Null byte injection attempt
	}

	for _, path := range unicodePaths {
		_, err := validator.ValidatePath(path)
		if err == nil {
			t.Logf("Unicode path allowed (may be ok depending on handling): %s", path)
		}
	}
}

// TestFIFOPathHandling tests handling of FIFO special files
func TestSecurePathValidator_FIFOPathHandling(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("Skipping: FIFO handling test requires elevated permissions")
	}

	tempDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	// Create a FIFO file
	fifoPath := filepath.Join(tempDir, "test.fifo")
	if err := os.MkdirAll(filepath.Dir(fifoPath), 0755); err != nil {
		t.Fatalf("Failed to create directory: %v", err)
	}

	// Attempt to validate path with FIFO - should handle gracefully
	_, err := validator.ValidatePath("test.fifo")
	// FIFO doesn't exist yet, so this might error for that reason
	// The important thing is it doesn't panic or allow escape
	t.Logf("FIFO path validation result: %v", err)
}

// TestRaceConditionSymlinkSwitching tests TOCTOU (Time-of-Check Time-of-Use) attacks
func TestSecurePathValidator_RaceConditionSymlinkSwitching(t *testing.T) {
	tempDir := t.TempDir()
	outsideDir := t.TempDir()
	validator := fileutil.NewSecurePathValidator(tempDir)

	// Create legitimate target file
	legitFile := filepath.Join(tempDir, "legit.txt")
	if err := os.WriteFile(legitFile, []byte("legitimate"), 0600); err != nil {
		t.Fatalf("Failed to create legit file: %v", err)
	}

	// Create outside file
	outsideFile := filepath.Join(outsideDir, "secret.txt")
	if err := os.WriteFile(outsideFile, []byte("secret"), 0600); err != nil {
		t.Fatalf("Failed to create outside file: %v", err)
	}

	// Create initial symlink pointing to legitimate file
	symlinkPath := filepath.Join(tempDir, "race_link")
	if err := os.Symlink(legitFile, symlinkPath); err != nil {
		t.Fatalf("Failed to create symlink: %v", err)
	}

	// Verify initial access works
	_, err := validator.ValidatePath("race_link")
	if err != nil {
		t.Logf("Initial symlink validation: %v", err)
	}

	// Race condition simulation: swap symlink target
	// In a real attack, this would happen between check and use
	os.Remove(symlinkPath)
	if err := os.Symlink(outsideFile, symlinkPath); err != nil {
		t.Fatalf("Failed to swap symlink: %v", err)
	}

	// Second validation should still detect the escape
	_, err = validator.ValidatePath("race_link")
	if err == nil {
		t.Errorf("Symlink switch should be detected and blocked: path escapes base directory")
	}
}
