using System.IO; using System.Linq; using SongTidy.Models; using SongTidy.Services; using SongTidy.ViewModels; namespace SongTidy.Tests; /// /// Contains tests for . /// [TestClass] public sealed class MainWindowViewModelTests { private string _tempDirectory = string.Empty; /// /// Creates a unique temporary directory for the current test. /// [TestInitialize] public void Initialize() { _tempDirectory = Path.Combine(Path.GetTempPath(), $"SongTidy ViewModel {Guid.NewGuid():N}"); Directory.CreateDirectory(_tempDirectory); } /// /// Removes the temporary directory created for the current test. /// [TestCleanup] public void Cleanup() { Directory.Delete(_tempDirectory, recursive: true); } #region Input and merge rules /// /// Verifies that added files are parsed and appended. /// [TestMethod] public void AddFilesParsesAndAppendsEntries() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3"); viewModel.AddFiles(new[] { sourcePath }); Assert.HasCount(1, viewModel.Entries); Assert.AreEqual(1, viewModel.EntryCount); Assert.IsTrue(viewModel.IsDirty); SongEntryViewModel entry = viewModel.Entries[0]; Assert.AreEqual(sourcePath, entry.FilePath); Assert.AreEqual("A", entry.Artist); Assert.AreEqual("B", entry.Title); Assert.AreEqual("Live", entry.Comment); Assert.AreEqual("A - B (Live).mp3", entry.OriginalFileName); Assert.AreEqual("A - B (Live).mp3", entry.TargetFileName); } /// /// Verifies that duplicate paths keep the existing entry and its edits. /// [TestMethod] public void AddFilesSkipsDuplicatesAndPreservesEdits() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A - B.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.Entries[0].Artist = "Edited"; viewModel.AddFiles(new[] { sourcePath }); Assert.HasCount(1, viewModel.Entries); Assert.AreEqual("Edited", viewModel.Entries[0].Artist); } /// /// Verifies that duplicate detection compares full path strings case-sensitively. /// [TestMethod] public void AddFilesTreatsPathsAsCaseSensitive() { MainWindowViewModel viewModel = new(); viewModel.AddFiles(new[] { Path.Combine(_tempDirectory, "a.mp3"), Path.Combine(_tempDirectory, "A.mp3"), }); Assert.HasCount(2, viewModel.Entries); } /// /// Verifies that adding only duplicates does not change the dirty state. /// [TestMethod] public void AddFilesWithOnlyDuplicatesDoesNotDirty() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.AddFiles(new[] { sourcePath }); Assert.IsFalse(viewModel.IsDirty); } /// /// Verifies that folder input uses the audio extension filter. /// [TestMethod] public void AddFilesFromFolderAddsOnlyAudioFiles() { MainWindowViewModel viewModel = new(); CreateFile("A.mp3"); CreateFile("B.FLAC"); CreateFile("C.txt"); CreateFile(Path.Combine("sub", "D.wav")); viewModel.AddFilesFromFolder(_tempDirectory); Assert.HasCount(2, viewModel.Entries); } #endregion #region Dirty tracking /// /// Verifies that field edits mark the document dirty and reverting does not clear it. /// [TestMethod] public void EntryEditMarksDirtyAndRevertingDoesNotClear() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); SongEntryViewModel entry = viewModel.Entries[0]; string originalArtist = entry.Artist; entry.Artist = "Changed"; Assert.IsTrue(viewModel.IsDirty); entry.Artist = originalArtist; Assert.IsTrue(viewModel.IsDirty); } /// /// Verifies that changing the format updates previews and marks the document dirty. /// [TestMethod] public void FormatChangeUpdatesPreviewsAndMarksDirty() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SelectedFormat = RenameFormat.TitleFirst; Assert.IsTrue(viewModel.IsDirty); Assert.AreEqual("B (Live) - A.mp3", viewModel.Entries[0].TargetFileName); } /// /// Verifies that selecting the current format does not mark the document dirty. /// [TestMethod] public void SettingSameFormatDoesNotDirty() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.SelectedFormat = RenameFormat.ArtistFirst; Assert.IsFalse(viewModel.IsDirty); } #endregion #region Plan operations /// /// Verifies that starting a new plan clears entries, path, and dirty state. /// [TestMethod] public void NewPlanClearsDocument() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.NewPlan(); Assert.HasCount(0, viewModel.Entries); Assert.AreEqual(0, viewModel.EntryCount); Assert.IsNull(viewModel.PlanPath); Assert.IsFalse(viewModel.IsDirty); } /// /// Verifies that closing a plan clears entries, path, and dirty state. /// [TestMethod] public void ClosePlanClearsDocument() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A.mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SavePlan(Path.Combine(_tempDirectory, "plan.xml")); viewModel.ClosePlan(); Assert.HasCount(0, viewModel.Entries); Assert.AreEqual(0, viewModel.EntryCount); Assert.IsNull(viewModel.PlanPath); Assert.IsFalse(viewModel.IsDirty); } /// /// Verifies that loading a plan restores its format, entries, and path. /// [TestMethod] public void LoadPlanRestoresDocument() { string planPath = Path.Combine(_tempDirectory, "plan.xml"); SongPlan plan = new() { Format = RenameFormat.TitleFirst, Items = new List { new PlanItem { Path = Path.Combine(_tempDirectory, "A - B.mp3"), Artist = "A", Title = "B", }, new PlanItem { Path = Path.Combine(_tempDirectory, "C.mp3"), Title = "C", Comment = "Live", }, }, }; PlanXmlIO.Save(plan, planPath); MainWindowViewModel viewModel = new(); viewModel.LoadPlan(planPath); Assert.AreEqual(RenameFormat.TitleFirst, viewModel.SelectedFormat); Assert.HasCount(2, viewModel.Entries); Assert.AreEqual(2, viewModel.EntryCount); Assert.AreEqual(planPath, viewModel.PlanPath); Assert.IsFalse(viewModel.IsDirty); Assert.AreEqual("B - A.mp3", viewModel.Entries[0].TargetFileName); Assert.AreEqual("C (Live).mp3", viewModel.Entries[1].TargetFileName); } /// /// Verifies saving to a path and then using the existing-path save overload. /// [TestMethod] public void SavePlanWritesDocumentAndClearsDirty() { MainWindowViewModel viewModel = new(); string sourcePath = Path.Combine(_tempDirectory, "A - B (Live).mp3"); viewModel.AddFiles(new[] { sourcePath }); viewModel.SelectedFormat = RenameFormat.TitleFirst; string planPath = Path.Combine(_tempDirectory, "saved.xml"); viewModel.SavePlan(planPath); Assert.AreEqual(planPath, viewModel.PlanPath); Assert.IsFalse(viewModel.IsDirty); SongPlan saved = PlanXmlIO.Load(planPath); Assert.AreEqual(RenameFormat.TitleFirst, saved.Format); Assert.HasCount(1, saved.Items); Assert.AreEqual("A", saved.Items[0].Artist); Assert.AreEqual("B", saved.Items[0].Title); Assert.AreEqual("Live", saved.Items[0].Comment); viewModel.Entries[0].Comment = "Changed"; Assert.IsTrue(viewModel.IsDirty); Assert.IsTrue(viewModel.SavePlan()); Assert.IsFalse(viewModel.IsDirty); SongPlan reloaded = PlanXmlIO.Load(planPath); Assert.AreEqual("Changed", reloaded.Items[0].Comment); } #endregion #region Validation /// /// Verifies that validation uses the current entries. /// [TestMethod] public void ValidateRenameUsesCurrentDocument() { string sourcePath = CreateFile("A.mp3"); MainWindowViewModel viewModel = new(); viewModel.AddFiles(new[] { sourcePath }); viewModel.Entries[0].Title = string.Empty; RenameValidationResult result = viewModel.ValidateRename(); Assert.IsTrue(result.Issues.Any(issue => issue.Problem == RenameValidationProblem.EmptyTitle)); } #endregion /// /// Creates an empty file inside the test directory. /// /// The relative file name, which may include a subdirectory. /// The full path of the created file. private string CreateFile(string fileName) { string path = Path.Combine(_tempDirectory, fileName); string directory = Path.GetDirectoryName(path) ?? _tempDirectory; Directory.CreateDirectory(directory); File.WriteAllText(path, string.Empty); return path; } }