package fileutil

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"testing"
)

func TestMain(m *testing.M) {
	tmpRoot, err := os.MkdirTemp("", "fileutil-test-*")
	if err != nil {
		panic(err)
	}

	if err := os.Setenv("TMPDIR", tmpRoot); err != nil {
		panic(err)
	}
	if err := os.Setenv("HOME", tmpRoot); err != nil {
		panic(err)
	}
	if err := os.Setenv("USERPROFILE", tmpRoot); err != nil {
		panic(err)
	}

	code := m.Run()
	_ = os.RemoveAll(tmpRoot)
	os.Exit(code)
}

func mustMarshal(t *testing.T, v any) []byte {
	t.Helper()
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		t.Fatal(err)
	}
	return data
}

func isolatedTempDir(t *testing.T) string {
	t.Helper()
	return t.TempDir()
}

func TestWriteWithBackup(t *testing.T) {
	tmpDir := isolatedTempDir(t)

	t.Run("uses ollama directory under home", func(t *testing.T) {
		home := t.TempDir()
		t.Setenv("HOME", home)
		t.Setenv("USERPROFILE", home)

		want := filepath.Join(home, ".ollama", "backup")
		if got := BackupDir(); got != want {
			t.Fatalf("BackupDir() = %q, want %q", got, want)
		}
	})

	t.Run("creates file", func(t *testing.T) {
		path := filepath.Join(tmpDir, "new.json")
		data := mustMarshal(t, map[string]string{"key": "value"})

		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		content, err := os.ReadFile(path)
		if err != nil {
			t.Fatal(err)
		}

		var result map[string]string
		if err := json.Unmarshal(content, &result); err != nil {
			t.Fatal(err)
		}
		if result["key"] != "value" {
			t.Errorf("expected value, got %s", result["key"])
		}
	})

	t.Run("creates backup in the shared backup directory", func(t *testing.T) {
		path := filepath.Join(tmpDir, "backup.json")

		os.WriteFile(path, []byte(`{"original": true}`), 0o644)

		data := mustMarshal(t, map[string]bool{"updated": true})
		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		entries, err := os.ReadDir(BackupDir())
		if err != nil {
			t.Fatal("backup directory not created")
		}

		var foundBackup bool
		for _, entry := range entries {
			if filepath.Ext(entry.Name()) != ".json" {
				name := entry.Name()
				if len(name) > len("backup.json.") && name[:len("backup.json.")] == "backup.json." {
					backupPath := filepath.Join(BackupDir(), name)
					backup, err := os.ReadFile(backupPath)
					if err == nil {
						var backupData map[string]bool
						json.Unmarshal(backup, &backupData)
						if backupData["original"] {
							foundBackup = true
							os.Remove(backupPath)
							break
						}
					}
				}
			}
		}

		if !foundBackup {
			t.Error("backup file not created in backup directory")
		}

		current, _ := os.ReadFile(path)
		var currentData map[string]bool
		json.Unmarshal(current, &currentData)
		if !currentData["updated"] {
			t.Error("file doesn't contain updated data")
		}
	})

	t.Run("stores hinted backups under a subdirectory", func(t *testing.T) {
		path := filepath.Join(tmpDir, "hinted.json")
		os.WriteFile(path, []byte(`{"original": true}`), 0o644)

		data := mustMarshal(t, map[string]bool{"updated": true})
		if err := WriteWithBackup(path, data, "openclaw"); err != nil {
			t.Fatal(err)
		}

		entries, err := os.ReadDir(filepath.Join(BackupDir(), "openclaw"))
		if err != nil {
			t.Fatal(err)
		}

		var found bool
		for _, entry := range entries {
			name := entry.Name()
			if len(name) > len("hinted.json.") && name[:len("hinted.json.")] == "hinted.json." {
				found = true
				_ = os.Remove(filepath.Join(BackupDir(), "openclaw", name))
				break
			}
		}

		if !found {
			t.Error("backup file was not created under hint directory")
		}
	})

	t.Run("no backup for new file", func(t *testing.T) {
		path := filepath.Join(tmpDir, "nobak.json")

		data := mustMarshal(t, map[string]string{"new": "file"})
		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		entries, _ := os.ReadDir(BackupDir())
		for _, entry := range entries {
			if len(entry.Name()) > len("nobak.json.") && entry.Name()[:len("nobak.json.")] == "nobak.json." {
				t.Error("backup should not exist for new file")
			}
		}
	})

	t.Run("no backup when content unchanged", func(t *testing.T) {
		path := filepath.Join(tmpDir, "unchanged.json")

		data := mustMarshal(t, map[string]string{"key": "value"})

		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		entries1, _ := os.ReadDir(BackupDir())
		countBefore := 0
		for _, e := range entries1 {
			if len(e.Name()) > len("unchanged.json.") && e.Name()[:len("unchanged.json.")] == "unchanged.json." {
				countBefore++
			}
		}

		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		entries2, _ := os.ReadDir(BackupDir())
		countAfter := 0
		for _, e := range entries2 {
			if len(e.Name()) > len("unchanged.json.") && e.Name()[:len("unchanged.json.")] == "unchanged.json." {
				countAfter++
			}
		}

		if countAfter != countBefore {
			t.Errorf("backup was created when content unchanged (before=%d, after=%d)", countBefore, countAfter)
		}
	})

	t.Run("backup filename contains unix timestamp", func(t *testing.T) {
		path := filepath.Join(tmpDir, "timestamped.json")

		os.WriteFile(path, []byte(`{"v": 1}`), 0o644)
		data := mustMarshal(t, map[string]int{"v": 2})
		if err := WriteWithBackup(path, data); err != nil {
			t.Fatal(err)
		}

		entries, _ := os.ReadDir(BackupDir())
		var found bool
		for _, entry := range entries {
			name := entry.Name()
			if len(name) > len("timestamped.json.") && name[:len("timestamped.json.")] == "timestamped.json." {
				timestamp := name[len("timestamped.json."):]
				for _, c := range timestamp {
					if c < '0' || c > '9' {
						t.Errorf("backup filename timestamp contains non-numeric character: %s", name)
					}
				}
				found = true
				os.Remove(filepath.Join(BackupDir(), name))
				break
			}
		}
		if !found {
			t.Error("backup file with timestamp not found")
		}
	})

	t.Run("retains only the five newest backups per file", func(t *testing.T) {
		path := filepath.Join(tmpDir, "pruned.json")
		if err := os.WriteFile(path, []byte(`{"v": 0}`), 0o644); err != nil {
			t.Fatal(err)
		}

		for i := 1; i <= maxBackupsPerFile; i++ {
			backupPath := filepath.Join(BackupDir(), fmt.Sprintf("pruned.json.%d", i))
			if err := os.WriteFile(backupPath, []byte(fmt.Sprintf(`{"v": %d}`, i)), 0o644); err != nil {
				t.Fatal(err)
			}
		}

		if err := WriteWithBackup(path, []byte(`{"v": 1}`)); err != nil {
			t.Fatal(err)
		}

		backups, err := filepath.Glob(filepath.Join(BackupDir(), "pruned.json.*"))
		if err != nil {
			t.Fatal(err)
		}
		if len(backups) != maxBackupsPerFile {
			t.Fatalf("expected %d backups after pruning, got %d", maxBackupsPerFile, len(backups))
		}
		if _, err := os.Stat(filepath.Join(BackupDir(), "pruned.json.1")); !os.IsNotExist(err) {
			t.Fatalf("expected oldest backup to be pruned, stat err = %v", err)
		}
	})
}

// Edge case tests for files.go

// TestWriteWithBackup_FailsIfBackupFails documents critical behavior: if backup fails, we must not proceed.
// User could lose their config with no way to recover.
func TestWriteWithBackup_FailsIfBackupFails(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "config.json")

	// Create original file
	originalContent := []byte(`{"original": true}`)
	os.WriteFile(path, originalContent, 0o644)

	// Make backup directory read-only to force backup failure
	backupDir := BackupDir()
	os.MkdirAll(backupDir, 0o755)
	os.Chmod(backupDir, 0o444) // Read-only
	defer os.Chmod(backupDir, 0o755)

	newContent := []byte(`{"updated": true}`)
	err := WriteWithBackup(path, newContent)

	// Should fail because backup couldn't be created
	if err == nil {
		t.Error("expected error when backup fails, got nil")
	}

	// Original file should be preserved
	current, _ := os.ReadFile(path)
	if string(current) != string(originalContent) {
		t.Errorf("original file was modified despite backup failure: got %s", string(current))
	}
}

// TestWriteWithBackup_PermissionDenied verifies clear error when target file has wrong permissions.
// Common issue when config owned by root or wrong perms.
func TestWriteWithBackup_PermissionDenied(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)

	// Create a read-only directory
	readOnlyDir := filepath.Join(tmpDir, "readonly")
	os.MkdirAll(readOnlyDir, 0o755)
	os.Chmod(readOnlyDir, 0o444)
	defer os.Chmod(readOnlyDir, 0o755)

	path := filepath.Join(readOnlyDir, "config.json")
	err := WriteWithBackup(path, []byte(`{"test": true}`))

	if err == nil {
		t.Error("expected permission error, got nil")
	}
}

func TestWriteWithBackup_UnchangedContentIsNoOp(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "unchanged-noop.json")
	data := []byte(`{"same":true}`)
	if err := os.WriteFile(path, data, 0o644); err != nil {
		t.Fatal(err)
	}

	if err := os.Chmod(tmpDir, 0o555); err != nil {
		t.Fatal(err)
	}
	defer os.Chmod(tmpDir, 0o755)

	if err := WriteWithBackup(path, data); err != nil {
		t.Fatalf("expected unchanged write to be a no-op, got %v", err)
	}

	backups, err := filepath.Glob(filepath.Join(BackupDir(), "unchanged-noop.json.*"))
	if err != nil {
		t.Fatal(err)
	}
	if len(backups) != 0 {
		t.Fatalf("expected no backups for unchanged content, got %d", len(backups))
	}
}

// TestWriteWithBackup_DirectoryDoesNotExist verifies behavior when target directory doesn't exist.
// writeWithBackup doesn't create directories - caller is responsible.
func TestWriteWithBackup_DirectoryDoesNotExist(t *testing.T) {
	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "nonexistent", "subdir", "config.json")

	err := WriteWithBackup(path, []byte(`{"test": true}`))

	// Should fail because directory doesn't exist
	if err == nil {
		t.Error("expected error for nonexistent directory, got nil")
	}
}

// TestWriteWithBackup_SymlinkTarget documents behavior when target is a symlink.
// Documents what happens if user symlinks their config file.
func TestWriteWithBackup_SymlinkTarget(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("symlink tests may require admin on Windows")
	}

	tmpDir := isolatedTempDir(t)
	realFile := filepath.Join(tmpDir, "real.json")
	symlink := filepath.Join(tmpDir, "link.json")

	// Create real file and symlink
	os.WriteFile(realFile, []byte(`{"v": 1}`), 0o644)
	os.Symlink(realFile, symlink)

	// Write through symlink
	err := WriteWithBackup(symlink, []byte(`{"v": 2}`))
	if err != nil {
		t.Fatalf("writeWithBackup through symlink failed: %v", err)
	}

	// The real file should be updated (symlink followed for temp file creation)
	content, _ := os.ReadFile(symlink)
	if string(content) != `{"v": 2}` {
		t.Errorf("symlink target not updated correctly: got %s", string(content))
	}
}

// TestBackupToTmp_SpecialCharsInFilename verifies backup works with special characters.
// User may have config files with unusual names.
func TestBackupToTmp_SpecialCharsInFilename(t *testing.T) {
	tmpDir := isolatedTempDir(t)

	// File with spaces and special chars
	path := filepath.Join(tmpDir, "my config (backup).json")
	os.WriteFile(path, []byte(`{"test": true}`), 0o644)

	backupPath, err := writeBackupCopy(path, "")
	if err != nil {
		t.Fatalf("writeBackupCopy with special chars failed: %v", err)
	}

	// Verify backup exists and has correct content
	content, err := os.ReadFile(backupPath)
	if err != nil {
		t.Fatalf("could not read backup: %v", err)
	}
	if string(content) != `{"test": true}` {
		t.Errorf("backup content mismatch: got %s", string(content))
	}

	os.Remove(backupPath)
}

// TestCopyFile_PreservesPermissions verifies that copyFile preserves file permissions.
func TestCopyFile_PreservesPermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission preservation tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)
	src := filepath.Join(tmpDir, "src.json")
	dst := filepath.Join(tmpDir, "dst.json")

	// Create source with specific permissions
	os.WriteFile(src, []byte(`{"test": true}`), 0o600)

	err := copyFile(src, dst)
	if err != nil {
		t.Fatalf("copyFile failed: %v", err)
	}

	srcInfo, _ := os.Stat(src)
	dstInfo, _ := os.Stat(dst)

	if srcInfo.Mode().Perm() != dstInfo.Mode().Perm() {
		t.Errorf("permissions not preserved: src=%v, dst=%v", srcInfo.Mode().Perm(), dstInfo.Mode().Perm())
	}
}

// TestCopyFile_SourceNotFound verifies clear error when source doesn't exist.
func TestCopyFile_SourceNotFound(t *testing.T) {
	tmpDir := isolatedTempDir(t)
	src := filepath.Join(tmpDir, "nonexistent.json")
	dst := filepath.Join(tmpDir, "dst.json")

	err := copyFile(src, dst)
	if err == nil {
		t.Error("expected error for nonexistent source, got nil")
	}
}

// TestWriteWithBackup_TargetIsDirectory verifies error when path points to a directory.
func TestWriteWithBackup_TargetIsDirectory(t *testing.T) {
	tmpDir := isolatedTempDir(t)
	dirPath := filepath.Join(tmpDir, "actualdir")
	os.MkdirAll(dirPath, 0o755)

	err := WriteWithBackup(dirPath, []byte(`{"test": true}`))
	if err == nil {
		t.Error("expected error when target is a directory, got nil")
	}
}

// TestWriteWithBackup_EmptyData verifies writing zero bytes works correctly.
func TestWriteWithBackup_EmptyData(t *testing.T) {
	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "empty.json")

	err := WriteWithBackup(path, []byte{})
	if err != nil {
		t.Fatalf("writeWithBackup with empty data failed: %v", err)
	}

	content, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("could not read file: %v", err)
	}
	if len(content) != 0 {
		t.Errorf("expected empty file, got %d bytes", len(content))
	}
}

// TestWriteWithBackup_FileUnreadableButDirWritable verifies behavior when existing file
// cannot be read (for backup comparison) but directory is writable.
func TestWriteWithBackup_FileUnreadableButDirWritable(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "unreadable.json")

	// Create file and make it unreadable
	os.WriteFile(path, []byte(`{"original": true}`), 0o644)
	os.Chmod(path, 0o000)
	defer os.Chmod(path, 0o644)

	// Should fail because we can't read the file to compare/backup
	err := WriteWithBackup(path, []byte(`{"updated": true}`))
	if err == nil {
		t.Error("expected error when file is unreadable, got nil")
	}
}

// TestWriteWithBackup_RapidSuccessiveWrites verifies backup works with multiple writes
// within the same second (timestamp collision scenario).
func TestWriteWithBackup_RapidSuccessiveWrites(t *testing.T) {
	tmpDir := isolatedTempDir(t)
	path := filepath.Join(tmpDir, "rapid.json")

	// Create initial file
	os.WriteFile(path, []byte(`{"v": 0}`), 0o644)

	// Rapid successive writes
	for i := 1; i <= 3; i++ {
		data := []byte(fmt.Sprintf(`{"v": %d}`, i))
		if err := WriteWithBackup(path, data); err != nil {
			t.Fatalf("write %d failed: %v", i, err)
		}
	}

	// Verify final content
	content, _ := os.ReadFile(path)
	if string(content) != `{"v": 3}` {
		t.Errorf("expected final content {\"v\": 3}, got %s", string(content))
	}

	// Verify at least one backup exists
	entries, _ := os.ReadDir(BackupDir())
	var backupCount int
	for _, e := range entries {
		if len(e.Name()) > len("rapid.json.") && e.Name()[:len("rapid.json.")] == "rapid.json." {
			backupCount++
		}
	}
	if backupCount == 0 {
		t.Error("expected at least one backup file from rapid writes")
	}
}

// TestWriteWithBackup_BackupDirIsFile verifies error when backup directory path is a file.
func TestWriteWithBackup_BackupDirIsFile(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("test modifies system temp directory")
	}

	tmpDir := isolatedTempDir(t)
	// Create a file at the backup directory path
	backupPath := BackupDir()
	// Clean up any existing directory first
	os.RemoveAll(backupPath)
	// Create a file instead of directory
	os.WriteFile(backupPath, []byte("not a directory"), 0o644)
	defer func() {
		os.Remove(backupPath)
		os.MkdirAll(backupPath, 0o755)
	}()

	path := filepath.Join(tmpDir, "test.json")
	os.WriteFile(path, []byte(`{"original": true}`), 0o644)

	err := WriteWithBackup(path, []byte(`{"updated": true}`))
	if err == nil {
		t.Error("expected error when backup dir is a file, got nil")
	}
}

// TestWriteWithBackup_NoOrphanTempFiles verifies temp files are cleaned up on failure.
func TestWriteWithBackup_NoOrphanTempFiles(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("permission tests unreliable on Windows")
	}

	tmpDir := isolatedTempDir(t)

	// Count existing temp files
	countTempFiles := func() int {
		entries, _ := os.ReadDir(tmpDir)
		count := 0
		for _, e := range entries {
			if len(e.Name()) > 4 && e.Name()[:4] == ".tmp" {
				count++
			}
		}
		return count
	}

	before := countTempFiles()

	// Create a file, then make directory read-only to cause rename failure
	path := filepath.Join(tmpDir, "orphan.json")
	os.WriteFile(path, []byte(`{"v": 1}`), 0o644)

	// Make a subdirectory and try to write there after making parent read-only
	subDir := filepath.Join(tmpDir, "subdir")
	os.MkdirAll(subDir, 0o755)
	subPath := filepath.Join(subDir, "config.json")
	os.WriteFile(subPath, []byte(`{"v": 1}`), 0o644)

	// Make subdir read-only after creating temp file would succeed but rename would fail
	// This is tricky to test - the temp file is created in the same dir, so if we can't
	// rename, we also couldn't create. Let's just verify normal failure cleanup works.

	// Force a failure by making the target a directory
	badPath := filepath.Join(tmpDir, "isdir")
	os.MkdirAll(badPath, 0o755)

	_ = WriteWithBackup(badPath, []byte(`{"test": true}`))

	after := countTempFiles()
	if after > before {
		t.Errorf("orphan temp files left behind: before=%d, after=%d", before, after)
	}
}
