package uiconfig import ( "os" "path/filepath" "testing" "gitgud.io/mike/mpv-manager/internal/fileops" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/modernzconf" "github.com/stretchr/testify/require" ) func sandboxUIConfig(t *testing.T, uiType, confName, confContent string) string { t.Helper() home := t.TempDir() for _, key := range []string{"HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME"} { t.Setenv(key, home) } _, err := config.Reload() require.NoError(t, err) configDir := config.GetMPVConfigPath() require.NoError(t, os.MkdirAll(filepath.Join(configDir, constants.ScriptsDir), 0755)) require.NoError(t, os.MkdirAll(filepath.Join(configDir, constants.ScriptOptsDir), 0755)) scriptName := "modernz.lua" if uiType == constants.UITypeUOSC { scriptName = "uosc.lua" } require.NoError(t, os.WriteFile(filepath.Join(configDir, constants.ScriptsDir, scriptName), []byte("-- test"), 0644)) if confName != "" { require.NoError(t, os.WriteFile(filepath.Join(configDir, constants.ScriptOptsDir, confName), []byte(confContent), 0644)) } return configDir } func TestModernZScalingMigrationApplyIsPersistent(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=auto\nscalewindowed=1.5\n") status, err := Inspect() require.NoError(t, err) require.NotNil(t, status.Migration) require.Equal(t, MigrationModernZScalingV1, status.Migration.ID) require.NoError(t, ResolveMigration(MigrationModernZScalingV1, MigrationActionApply, "")) conf, err := modernzconf.ParseModernZConf(filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf")) require.NoError(t, err) require.Equal(t, modernzconf.ManagedVidscaleDefault, conf.GetValue("vidscale")) require.Equal(t, "1.5", conf.GetValue("scalewindowed"), "migration must not replace custom size multipliers") require.Equal(t, MigrationActionApply, config.GetUIConfigMigrationResolution(MigrationModernZScalingV1)) status, err = Inspect() require.NoError(t, err) require.Nil(t, status.Migration) } func TestModernZScalingMigrationKeepDoesNotRewriteConfig(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "# custom\nvidscale=auto\n") path := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") before, err := os.ReadFile(path) require.NoError(t, err) require.NoError(t, ResolveMigration(MigrationModernZScalingV1, MigrationActionKeep, "")) after, err := os.ReadFile(path) require.NoError(t, err) require.Equal(t, before, after) require.Equal(t, MigrationActionKeep, config.GetUIConfigMigrationResolution(MigrationModernZScalingV1)) } func TestInspectReportsInvalidUOSCValues(t *testing.T) { sandboxUIConfig(t, constants.UITypeUOSC, "uosc.conf", "timeline_size=999\n") status, err := Inspect() require.NoError(t, err) require.Equal(t, constants.UITypeUOSC, status.UI) require.Len(t, status.Warnings, 1) require.Equal(t, "timeline_size", status.Warnings[0].Key) require.Nil(t, status.Migration) } func TestInspectRepresentsTwoInstalledOverlayScriptsAsAmbiguous(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=yes\n") require.NoError(t, os.WriteFile( filepath.Join(configDir, constants.ScriptsDir, "uosc.lua"), []byte("-- second overlay"), 0644, )) require.Equal(t, constants.UITypeAmbiguous, DetectInstalledUI()) status, err := Inspect() require.NoError(t, err) require.Equal(t, constants.UITypeAmbiguous, status.UI) require.Len(t, status.Warnings, 1) require.Equal(t, "ui", status.Warnings[0].Key) require.Contains(t, status.Warnings[0].Message, "Both ModernZ and uOSC") } func TestMigrationDoesNotOverrideExplicitWindowScaling(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=yes\n") status, err := Inspect() require.NoError(t, err) require.Nil(t, status.Migration) require.Error(t, ResolveMigration(MigrationModernZScalingV1, MigrationActionApply, "")) conf, err := modernzconf.ParseModernZConf(filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf")) require.NoError(t, err) require.Equal(t, "yes", conf.GetValue("vidscale")) } func TestMigrationRollsBackScriptWhenManagerPersistenceFails(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "# exact\nvidscale=auto\nscalewindowed=1.5\n") require.NoError(t, config.Save()) confPath := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") before, err := os.ReadFile(confPath) require.NoError(t, err) managerPath := filepath.Join(config.GetConfigDir(), constants.ConfigFileName) managerBefore, err := os.ReadFile(managerPath) require.NoError(t, err) require.NoError(t, os.Remove(managerPath)) require.NoError(t, os.Mkdir(managerPath, 0o755)) err = ResolveMigration(MigrationModernZScalingV1, MigrationActionApply, "") require.Error(t, err) after, readErr := os.ReadFile(confPath) require.NoError(t, readErr) require.Equal(t, before, after) require.NoFileExists(t, migrationJournalPath(confPath)) require.NoError(t, os.Remove(managerPath)) require.NoError(t, os.WriteFile(managerPath, managerBefore, 0o644)) _, err = config.Reload() require.NoError(t, err) } func TestMigrationRecoveryRollsBackUncommittedScriptChange(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=auto\n") confPath := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") before, err := os.ReadFile(confPath) require.NoError(t, err) journal, err := captureMigrationJournal(confPath, MigrationActionApply) require.NoError(t, err) require.NoError(t, writeMigrationJournal(confPath, journal)) require.NoError(t, fileops.WithLock(confPath, func() error { return modernzconf.SetValueFileLocked(confPath, "vidscale", modernzconf.ManagedVidscaleDefault) })) require.NoError(t, RecoverMigrationTransactions()) after, err := os.ReadFile(confPath) require.NoError(t, err) require.Equal(t, before, after) require.NoFileExists(t, migrationJournalPath(confPath)) } func TestMigrationRecoveryKeepsCommittedScriptChange(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=auto\n") confPath := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") journal, err := captureMigrationJournal(confPath, MigrationActionApply) require.NoError(t, err) require.NoError(t, writeMigrationJournal(confPath, journal)) require.NoError(t, fileops.WithLock(confPath, func() error { return modernzconf.SetValueFileLocked(confPath, "vidscale", modernzconf.ManagedVidscaleDefault) })) require.NoError(t, config.SetUIConfigMigrationResolution(MigrationModernZScalingV1, MigrationActionApply)) require.NoError(t, RecoverMigrationTransactions()) conf, err := modernzconf.ParseModernZConf(confPath) require.NoError(t, err) require.Equal(t, modernzconf.ManagedVidscaleDefault, conf.GetValue("vidscale")) require.NoFileExists(t, migrationJournalPath(confPath)) } func TestMigrationRecoveryRetainsConcurrentEditsAndJournal(t *testing.T) { for _, originalExists := range []bool{false, true} { t.Run(map[bool]string{false: "created after migration", true: "edited after migration"}[originalExists], func(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=auto\nscalewindowed=1.5\n") path := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") if !originalExists { require.NoError(t, os.Remove(path)) } journal, err := captureMigrationJournal(path, MigrationActionApply) require.NoError(t, err) require.NoError(t, writeMigrationJournal(path, journal)) require.NoError(t, modernzconf.SetValueFile(path, "vidscale", modernzconf.ManagedVidscaleDefault)) require.NoError(t, modernzconf.SetValueFile(path, "scalewindowed", "2.5")) before, err := os.ReadFile(path) require.NoError(t, err) require.ErrorContains(t, RecoverMigrationTransactions(), "changed during migration") after, err := os.ReadFile(path) require.NoError(t, err) require.Equal(t, before, after) require.FileExists(t, migrationJournalPath(path)) }) } } func TestMigrationRecoveryReplaysAlreadyRestoredSnapshot(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "# original\nvidscale=auto\n") path := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") journal, err := captureMigrationJournal(path, MigrationActionApply) require.NoError(t, err) require.NoError(t, writeMigrationJournal(path, journal)) require.NoError(t, RecoverMigrationTransactions()) require.NoError(t, RecoverMigrationTransactions()) require.NoFileExists(t, migrationJournalPath(path)) } func TestLegacyMigrationRecoveryRetainsAmbiguousSnapshot(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "vidscale=auto\n") path := filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf") journal, err := captureMigrationJournal(path, MigrationActionApply) require.NoError(t, err) journal.Schema = 1 journal.AppliedDigest = "" require.NoError(t, writeMigrationJournal(path, journal)) require.NoError(t, modernzconf.SetValueFile(path, "vidscale", "no")) require.ErrorContains(t, RecoverMigrationTransactions(), "recovery requires review") require.FileExists(t, migrationJournalPath(path)) } func TestQueuedMigrationMatchesLastPersistedValue(t *testing.T) { for _, otherEditor := range []bool{false, true} { t.Run(map[bool]string{false: "queued decision", true: "concurrent editor"}[otherEditor], func(t *testing.T) { configDir := sandboxUIConfig(t, constants.UITypeModernZ, "modernz.conf", "# retain\nvidscale=yes\nscalewindowed=1.5\n") expected := "yes" if otherEditor { expected = "auto" } err := ResolveMigration(MigrationModernZScalingV1, MigrationActionApply, expected) if otherEditor { require.ErrorContains(t, err, "scaling changed") } else { require.NoError(t, err) } conf, err := modernzconf.ParseModernZConf(filepath.Join(configDir, constants.ScriptOptsDir, "modernz.conf")) require.NoError(t, err) want := "no" if otherEditor { want = "yes" } require.Equal(t, want, conf.GetValue("vidscale")) require.Equal(t, "1.5", conf.GetValue("scalewindowed")) }) } } func TestDetectUOSCDirectoryEntrypoint(t *testing.T) { dir := sandboxUIConfig(t, constants.UITypeUOSC, "", "") require.NoError(t, os.Remove(filepath.Join(dir, "scripts", "uosc.lua"))) require.NoError(t, os.MkdirAll(filepath.Join(dir, "scripts", "uosc"), 0755)) require.NoError(t, os.WriteFile(filepath.Join(dir, "scripts", "uosc", "main.lua"), []byte("-- uosc"), 0644)) require.Equal(t, constants.UITypeUOSC, DetectInstalledUI()) require.NoError(t, os.WriteFile(filepath.Join(dir, "scripts", "modernz.lua"), []byte("-- modernz"), 0644)) require.Equal(t, constants.UITypeAmbiguous, DetectInstalledUI()) }