package config

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

func TestNewImportTransaction(t *testing.T) {
	t.Run("creates staging directory", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		if tx.stagingDir == "" {
			t.Error("stagingDir should not be empty")
		}

		// Verify staging directory exists
		info, err := os.Stat(tx.stagingDir)
		if err != nil {
			t.Errorf("staging directory should exist: %v", err)
		}
		if !info.IsDir() {
			t.Error("stagingDir should be a directory")
		}

		// Verify staging dir is under config dir
		if !strings.HasPrefix(tx.stagingDir, tmpDir) {
			t.Errorf("stagingDir %q should be under configDir %q", tx.stagingDir, tmpDir)
		}

		// Verify staging dir contains expected prefix
		if !strings.Contains(tx.stagingDir, ".import-staging-") {
			t.Errorf("stagingDir %q should contain .import-staging- prefix", tx.stagingDir)
		}
	})

	t.Run("initializes maps", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		if tx.staged == nil {
			t.Error("staged map should be initialized")
		}
		if tx.backups == nil {
			t.Error("backups map should be initialized")
		}
	})

	t.Run("sets configDir", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		if tx.configDir != tmpDir {
			t.Errorf("configDir = %q, want %q", tx.configDir, tmpDir)
		}
	})

	t.Run("sets timestamp", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		if tx.timestamp == "" {
			t.Error("timestamp should not be empty")
		}

		// Verify timestamp format (YYYYMMDD-HHMMSS)
		if len(tx.timestamp) != 15 {
			t.Errorf("timestamp length = %d, expected 15", len(tx.timestamp))
		}
		if tx.timestamp[8] != '-' {
			t.Errorf("timestamp format incorrect, expected dash at position 8: %q", tx.timestamp)
		}
	})

	t.Run("fails with invalid directory", func(t *testing.T) {
		_, err := newImportTransaction("/nonexistent/path/that/does/not/exist")
		if err == nil {
			t.Error("expected error for nonexistent directory")
		}
	})
}

func TestImportTransaction_StageFile(t *testing.T) {
	t.Run("stages file successfully", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "test.txt")
		data := []byte("test content")

		err = tx.StageFile(targetPath, data, 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		// Verify file is staged
		if _, ok := tx.staged[targetPath]; !ok {
			t.Error("file should be recorded in staged map")
		}

		// Verify staged file exists and has correct content
		stagedPath := tx.staged[targetPath]
		content, err := os.ReadFile(stagedPath)
		if err != nil {
			t.Fatalf("failed to read staged file: %v", err)
		}
		if string(content) != "test content" {
			t.Errorf("staged content = %q, want %q", string(content), "test content")
		}
	})

	t.Run("replaces previously staged file", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "test.txt")

		// Stage first version
		err = tx.StageFile(targetPath, []byte("version 1"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}
		firstStagedPath := tx.staged[targetPath]

		// Stage second version
		err = tx.StageFile(targetPath, []byte("version 2"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}
		secondStagedPath := tx.staged[targetPath]

		// Verify old staged file was removed
		if firstStagedPath == secondStagedPath {
			t.Error("second staged file should have different path")
		}
		if _, err := os.Stat(firstStagedPath); !os.IsNotExist(err) {
			t.Error("first staged file should be removed")
		}

		// Verify new content
		content, err := os.ReadFile(secondStagedPath)
		if err != nil {
			t.Fatalf("failed to read staged file: %v", err)
		}
		if string(content) != "version 2" {
			t.Errorf("staged content = %q, want %q", string(content), "version 2")
		}
	})

	t.Run("sets file permissions", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "test.txt")
		err = tx.StageFile(targetPath, []byte("test"), 0o600)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		stagedPath := tx.staged[targetPath]
		info, err := os.Stat(stagedPath)
		if err != nil {
			t.Fatalf("failed to stat staged file: %v", err)
		}

		// Check permissions (mask with 0777 to ignore umask effects)
		perm := info.Mode().Perm() & 0o777
		if perm != 0o600 {
			t.Errorf("file permissions = %o, want %o", perm, 0o600)
		}
	})

	t.Run("fails after commit", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		// Commit empty transaction
		err = tx.Commit()
		if err != nil {
			t.Fatalf("Commit() error = %v", err)
		}

		// Try to stage after commit
		targetPath := filepath.Join(tmpDir, "test.txt")
		err = tx.StageFile(targetPath, []byte("test"), 0o644)
		if err == nil {
			t.Error("expected error when staging after commit")
		}
		if !strings.Contains(err.Error(), "already committed") {
			t.Errorf("error should mention already committed: %v", err)
		}
	})

	t.Run("handles empty target base name", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		// Stage with target ending in separator (edge case)
		targetPath := filepath.Join(tmpDir, "subdir") + string(os.PathSeparator)
		err = tx.StageFile(targetPath, []byte("test"), 0o644)
		// Should not panic
		if err == nil {
			// It staged something - verify it's recorded
			if _, ok := tx.staged[targetPath]; !ok {
				t.Error("file should be recorded even with edge case path")
			}
		}
	})
}

func TestImportTransaction_Commit(t *testing.T) {
	t.Run("applies staged files", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "committed.txt")
		err = tx.StageFile(targetPath, []byte("committed content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		err = tx.Commit()
		if err != nil {
			t.Fatalf("Commit() error = %v", err)
		}

		// Verify file exists at target
		content, err := os.ReadFile(targetPath)
		if err != nil {
			t.Fatalf("failed to read committed file: %v", err)
		}
		if string(content) != "committed content" {
			t.Errorf("committed content = %q, want %q", string(content), "committed content")
		}
	})

	t.Run("creates backup of existing file", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create existing file
		targetPath := filepath.Join(tmpDir, "existing.txt")
		err := os.WriteFile(targetPath, []byte("original content"), 0o644)
		if err != nil {
			t.Fatalf("failed to create existing file: %v", err)
		}

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		err = tx.StageFile(targetPath, []byte("new content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		err = tx.Commit()
		if err != nil {
			t.Fatalf("Commit() error = %v", err)
		}

		// Verify new content
		content, err := os.ReadFile(targetPath)
		if err != nil {
			t.Fatalf("failed to read committed file: %v", err)
		}
		if string(content) != "new content" {
			t.Errorf("committed content = %q, want %q", string(content), "new content")
		}

		// Verify backup was removed on successful commit
		matches, _ := filepath.Glob(targetPath + ".import-backup-*")
		if len(matches) > 0 {
			t.Error("backup file should be removed after successful commit")
		}
	})

	t.Run("creates target directory if needed", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		// Target in non-existent subdirectory
		targetPath := filepath.Join(tmpDir, "subdir", "nested", "file.txt")
		err = tx.StageFile(targetPath, []byte("nested content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		err = tx.Commit()
		if err != nil {
			t.Fatalf("Commit() error = %v", err)
		}

		// Verify file exists
		content, err := os.ReadFile(targetPath)
		if err != nil {
			t.Fatalf("failed to read committed file: %v", err)
		}
		if string(content) != "nested content" {
			t.Errorf("committed content = %q, want %q", string(content), "nested content")
		}
	})

	t.Run("fails if destination is directory", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create directory at target path
		targetPath := filepath.Join(tmpDir, "targetdir")
		err := os.Mkdir(targetPath, 0o755)
		if err != nil {
			t.Fatalf("failed to create directory: %v", err)
		}

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		err = tx.StageFile(targetPath, []byte("content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		err = tx.Commit()
		if err == nil {
			t.Error("expected error when committing to directory")
		}
		if !strings.Contains(err.Error(), "is a directory") {
			t.Errorf("error should mention directory: %v", err)
		}
	})

	t.Run("cannot commit twice", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		err = tx.Commit()
		if err != nil {
			t.Fatalf("first Commit() error = %v", err)
		}

		err = tx.Commit()
		if err == nil {
			t.Error("expected error on second commit")
		}
		if !strings.Contains(err.Error(), "already committed") {
			t.Errorf("error should mention already committed: %v", err)
		}
	})

	t.Run("commits multiple files atomically", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		// Stage multiple files
		files := map[string]string{
			filepath.Join(tmpDir, "file1.txt"): "content 1",
			filepath.Join(tmpDir, "file2.txt"): "content 2",
			filepath.Join(tmpDir, "file3.txt"): "content 3",
		}

		for path, content := range files {
			err = tx.StageFile(path, []byte(content), 0o644)
			if err != nil {
				t.Fatalf("StageFile(%s) error = %v", path, err)
			}
		}

		err = tx.Commit()
		if err != nil {
			t.Fatalf("Commit() error = %v", err)
		}

		// Verify all files exist with correct content
		for path, expectedContent := range files {
			content, err := os.ReadFile(path)
			if err != nil {
				t.Errorf("failed to read %s: %v", path, err)
				continue
			}
			if string(content) != expectedContent {
				t.Errorf("content of %s = %q, want %q", path, string(content), expectedContent)
			}
		}
	})
}

func TestImportTransaction_Rollback(t *testing.T) {
	t.Run("removes staged files", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "test.txt")
		err = tx.StageFile(targetPath, []byte("content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		stagedPath := tx.staged[targetPath]

		// Verify staged file exists
		if _, err := os.Stat(stagedPath); err != nil {
			t.Fatalf("staged file should exist: %v", err)
		}

		tx.Rollback()

		// Verify staged file is removed
		if _, err := os.Stat(stagedPath); !os.IsNotExist(err) {
			t.Error("staged file should be removed after rollback")
		}
	})

	t.Run("restores backup files", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create existing file
		targetPath := filepath.Join(tmpDir, "existing.txt")
		err := os.WriteFile(targetPath, []byte("original content"), 0o644)
		if err != nil {
			t.Fatalf("failed to create existing file: %v", err)
		}

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		err = tx.StageFile(targetPath, []byte("new content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		// Manually simulate partial commit state by creating backup
		backupPath := targetPath + ".import-backup-" + tx.timestamp
		err = os.Rename(targetPath, backupPath)
		if err != nil {
			t.Fatalf("failed to create backup: %v", err)
		}
		tx.backups[targetPath] = backupPath

		tx.Rollback()

		// Verify original file is restored
		content, err := os.ReadFile(targetPath)
		if err != nil {
			t.Fatalf("failed to read restored file: %v", err)
		}
		if string(content) != "original content" {
			t.Errorf("restored content = %q, want %q", string(content), "original content")
		}
	})

	t.Run("handles rollback with no backups", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		targetPath := filepath.Join(tmpDir, "new.txt")
		err = tx.StageFile(targetPath, []byte("content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		// Rollback should not panic even with no backups
		tx.Rollback()

		// Target should not exist
		if _, err := os.Stat(targetPath); !os.IsNotExist(err) {
			t.Error("target should not exist after rollback of new file")
		}
	})
}

func TestImportTransaction_Cleanup(t *testing.T) {
	t.Run("removes staging directory", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}

		stagingDir := tx.stagingDir

		// Verify staging directory exists
		if _, err := os.Stat(stagingDir); err != nil {
			t.Fatalf("staging directory should exist: %v", err)
		}

		tx.Cleanup()

		// Verify staging directory is removed
		if _, err := os.Stat(stagingDir); !os.IsNotExist(err) {
			t.Error("staging directory should be removed after cleanup")
		}
	})

	t.Run("removes staging directory with files", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}

		// Stage a file
		targetPath := filepath.Join(tmpDir, "test.txt")
		err = tx.StageFile(targetPath, []byte("content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		stagingDir := tx.stagingDir

		tx.Cleanup()

		// Verify staging directory is removed even with files
		if _, err := os.Stat(stagingDir); !os.IsNotExist(err) {
			t.Error("staging directory should be removed after cleanup")
		}
	})

	t.Run("cleanup is idempotent", func(t *testing.T) {
		tmpDir := t.TempDir()

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}

		// Call cleanup multiple times - should not panic
		tx.Cleanup()
		tx.Cleanup()
		tx.Cleanup()
	})
}

func TestImportTransaction_CommitRestoreOnFailure(t *testing.T) {
	t.Run("restores backups on commit failure", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create existing file
		existingPath := filepath.Join(tmpDir, "existing.txt")
		err := os.WriteFile(existingPath, []byte("original"), 0o644)
		if err != nil {
			t.Fatalf("failed to create existing file: %v", err)
		}

		tx, err := newImportTransaction(tmpDir)
		if err != nil {
			t.Fatalf("newImportTransaction() error = %v", err)
		}
		defer tx.Cleanup()

		// Stage replacement for existing file
		err = tx.StageFile(existingPath, []byte("replacement"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		// Stage file that will fail (target is a directory)
		failPath := filepath.Join(tmpDir, "willFail")
		err = os.Mkdir(failPath, 0o755)
		if err != nil {
			t.Fatalf("failed to create directory: %v", err)
		}
		err = tx.StageFile(failPath, []byte("content"), 0o644)
		if err != nil {
			t.Fatalf("StageFile() error = %v", err)
		}

		// Commit should fail
		err = tx.Commit()
		if err == nil {
			t.Fatal("expected commit to fail")
		}

		// Verify original file is restored
		content, err := os.ReadFile(existingPath)
		if err != nil {
			t.Fatalf("failed to read restored file: %v", err)
		}
		if string(content) != "original" {
			t.Errorf("restored content = %q, want %q", string(content), "original")
		}
	})
}
