package config_test

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

	"github.com/owncord/server/config"
)

// loadNoEnv loads cfgPath ensuring no OWNCORD_ env overrides leak in from the
// test environment.
func loadNoEnv(t *testing.T, cfgPath string) *config.Config {
	t.Helper()
	cfg, err := config.Load(cfgPath)
	if err != nil {
		t.Fatalf("Load() returned error: %v", err)
	}
	return cfg
}

func TestSavePatchesGeneratedDefaultFile(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")

	// Generate the default file the way first startup does.
	loadNoEnv(t, cfgPath)

	err := config.Save(cfgPath, config.Patch{
		ServerPort:      new(9000),
		ServerName:      new("My Cool Server"),
		TLSMode:         new("off"),
		UploadMaxSizeMB: new(250),
		VoiceQuality:    new("high"),
	})
	if err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}

	raw, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("reading patched file: %v", err)
	}
	text := string(raw)

	// Documentation comments must survive the round-trip.
	for _, want := range []string{
		"# OwnCord Server Configuration",
		"self_signed, acme, manual, off",
		"# telemetry:",
	} {
		if !strings.Contains(text, want) {
			t.Errorf("patched file lost comment %q", want)
		}
	}

	cfg := loadNoEnv(t, cfgPath)
	if cfg.Server.Port != 9000 {
		t.Errorf("Server.Port = %d, want 9000", cfg.Server.Port)
	}
	if cfg.Server.Name != "My Cool Server" {
		t.Errorf("Server.Name = %q, want 'My Cool Server'", cfg.Server.Name)
	}
	if cfg.TLS.Mode != "off" {
		t.Errorf("TLS.Mode = %q, want 'off'", cfg.TLS.Mode)
	}
	if cfg.Upload.MaxSizeMB != 250 {
		t.Errorf("Upload.MaxSizeMB = %d, want 250", cfg.Upload.MaxSizeMB)
	}
	if cfg.Voice.Quality != "high" {
		t.Errorf("Voice.Quality = %q, want 'high'", cfg.Voice.Quality)
	}
	// Untouched values keep their file defaults.
	if cfg.Database.Path != "data/chatserver.db" {
		t.Errorf("Database.Path = %q, want default", cfg.Database.Path)
	}
}

func TestSavePreservesHandEdits(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")

	handEdited := `# my important operator note
server:
  port: 8443
  name: "Old Name"
  trusted_proxies: ["10.0.0.2/32"]

gif:
  api_key: "klipy-secret"

custom_section:
  custom_key: 42
`
	if err := os.WriteFile(cfgPath, []byte(handEdited), 0o600); err != nil {
		t.Fatalf("writing hand-edited file: %v", err)
	}

	if err := config.Save(cfgPath, config.Patch{ServerName: new("New Name")}); err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}

	raw, err := os.ReadFile(cfgPath)
	if err != nil {
		t.Fatalf("reading patched file: %v", err)
	}
	text := string(raw)
	for _, want := range []string{
		"# my important operator note",
		"10.0.0.2/32",
		"klipy-secret",
		"custom_key: 42",
	} {
		if !strings.Contains(text, want) {
			t.Errorf("patched file lost hand-edited content %q", want)
		}
	}

	cfg := loadNoEnv(t, cfgPath)
	if cfg.Server.Name != "New Name" {
		t.Errorf("Server.Name = %q, want 'New Name'", cfg.Server.Name)
	}
	if cfg.Server.Port != 8443 {
		t.Errorf("Server.Port = %d, want 8443 (untouched)", cfg.Server.Port)
	}
	if cfg.GIF.APIKey != "klipy-secret" {
		t.Errorf("GIF.APIKey = %q, want preserved", cfg.GIF.APIKey)
	}
}

func TestSaveCreatesMissingFileFromTemplate(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")

	if err := config.Save(cfgPath, config.Patch{ServerPort: new(9999)}); err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}

	info, err := os.Stat(cfgPath)
	if err != nil {
		t.Fatalf("stat patched file: %v", err)
	}
	if runtime.GOOS != "windows" {
		if perm := info.Mode().Perm(); perm != 0o600 {
			t.Errorf("file mode = %o, want 0600", perm)
		}
	}

	raw, _ := os.ReadFile(cfgPath)
	if !strings.Contains(string(raw), "# OwnCord Server Configuration") {
		t.Error("file created from template lost the documentation header")
	}

	cfg := loadNoEnv(t, cfgPath)
	if cfg.Server.Port != 9999 {
		t.Errorf("Server.Port = %d, want 9999", cfg.Server.Port)
	}
}

func TestSaveVoiceCredentialsOnlyWhenEmpty(t *testing.T) {
	t.Run("written when absent", func(t *testing.T) {
		tmpDir := t.TempDir()
		cfgPath := filepath.Join(tmpDir, "config.yaml")
		loadNoEnv(t, cfgPath) // default file: livekit creds commented out

		err := config.Save(cfgPath, config.Patch{
			VoiceAPIKey:    new("key-abc123"),
			VoiceAPISecret: new("0123456789abcdef0123456789abcdef"),
		})
		if err != nil {
			t.Fatalf("Save() returned error: %v", err)
		}

		cfg := loadNoEnv(t, cfgPath)
		if cfg.Voice.LiveKitAPIKey != "key-abc123" {
			t.Errorf("LiveKitAPIKey = %q, want persisted key", cfg.Voice.LiveKitAPIKey)
		}
		if cfg.Voice.LiveKitAPISecret != "0123456789abcdef0123456789abcdef" {
			t.Errorf("LiveKitAPISecret = %q, want persisted secret", cfg.Voice.LiveKitAPISecret)
		}
	})

	t.Run("never overwritten when set", func(t *testing.T) {
		tmpDir := t.TempDir()
		cfgPath := filepath.Join(tmpDir, "config.yaml")
		existing := `voice:
  livekit_api_key: "operator-key"
  livekit_api_secret: "operator-secret-thats-32-chars-x"
`
		if err := os.WriteFile(cfgPath, []byte(existing), 0o600); err != nil {
			t.Fatalf("writing file: %v", err)
		}

		err := config.Save(cfgPath, config.Patch{
			VoiceAPIKey:    new("key-should-not-win"),
			VoiceAPISecret: new("secret-should-not-win-9876543210"),
		})
		if err != nil {
			t.Fatalf("Save() returned error: %v", err)
		}

		cfg := loadNoEnv(t, cfgPath)
		if cfg.Voice.LiveKitAPIKey != "operator-key" {
			t.Errorf("LiveKitAPIKey = %q, operator value was clobbered", cfg.Voice.LiveKitAPIKey)
		}
		if cfg.Voice.LiveKitAPISecret != "operator-secret-thats-32-chars-x" {
			t.Errorf("LiveKitAPISecret = %q, operator value was clobbered", cfg.Voice.LiveKitAPISecret)
		}
	})
}

func TestSaveVoiceAutoDownloadBool(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")
	loadNoEnv(t, cfgPath) // generated default file has auto_download_livekit: true

	if err := config.Save(cfgPath, config.Patch{VoiceAutoDownload: new(false)}); err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}
	cfg := loadNoEnv(t, cfgPath)
	if cfg.Voice.AutoDownloadLiveKit {
		t.Error("Voice.AutoDownloadLiveKit = true, want false after patch")
	}

	if err := config.Save(cfgPath, config.Patch{VoiceAutoDownload: new(true)}); err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}
	cfg = loadNoEnv(t, cfgPath)
	if !cfg.Voice.AutoDownloadLiveKit {
		t.Error("Voice.AutoDownloadLiveKit = false, want true after re-patch")
	}
}

func TestSaveYAMLInjectionIsInert(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")
	loadNoEnv(t, cfgPath)

	hostile := "x\nserver:\n  port: 1 # pwned"
	if err := config.Save(cfgPath, config.Patch{ServerName: new(hostile)}); err != nil {
		t.Fatalf("Save() returned error: %v", err)
	}

	cfg := loadNoEnv(t, cfgPath)
	if cfg.Server.Name != hostile {
		t.Errorf("Server.Name = %q, want the hostile string as one inert scalar", cfg.Server.Name)
	}
	if cfg.Server.Port != 8443 {
		t.Errorf("Server.Port = %d, want 8443 — injection changed a sibling key", cfg.Server.Port)
	}
}

func TestSaveLeavesUnparseableFileIntact(t *testing.T) {
	tmpDir := t.TempDir()
	cfgPath := filepath.Join(tmpDir, "config.yaml")

	for name, content := range map[string]string{
		"invalid yaml":     "\tserver: [",
		"root not mapping": "just a scalar\n",
	} {
		t.Run(name, func(t *testing.T) {
			if err := os.WriteFile(cfgPath, []byte(content), 0o600); err != nil {
				t.Fatalf("writing file: %v", err)
			}
			if err := config.Save(cfgPath, config.Patch{ServerPort: new(9000)}); err == nil {
				t.Fatal("Save() succeeded on a file it cannot safely patch")
			}
			raw, err := os.ReadFile(cfgPath)
			if err != nil {
				t.Fatalf("reading file: %v", err)
			}
			if string(raw) != content {
				t.Error("failed Save() modified the original file")
			}
		})
	}
}
