using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using SharepointToolbox.Core.Messages; using SharepointToolbox.Core.Models; using SharepointToolbox.Infrastructure.Auth; using SharepointToolbox.Infrastructure.Persistence; using SharepointToolbox.Services; using SharepointToolbox.Services.Export; using SharepointToolbox.ViewModels; using SharepointToolbox.ViewModels.Tabs; namespace SharepointToolbox.Tests.ViewModels; /// /// Unit tests for the global site selection flow (Phase 6). /// Covers: message broadcast, base class reception, single-site pre-fill, /// multi-site pre-populate, local override, override reset, tenant switch clear, /// and toolbar label update. /// Requirements: SITE-01, SITE-02 /// public class GlobalSiteSelectionTests { // ── Helper: minimal concrete subclass of FeatureViewModelBase ──────────── private class TestFeatureViewModel : FeatureViewModelBase { public TestFeatureViewModel(ILogger logger) : base(logger) { } protected override Task RunOperationAsync(CancellationToken ct, IProgress progress) => Task.CompletedTask; /// Expose protected GlobalSites for assertions. public IReadOnlyList TestGlobalSites => GlobalSites; } // ── Reset messenger between tests to avoid cross-test contamination ────── public GlobalSiteSelectionTests() { WeakReferenceMessenger.Default.Reset(); } // ── Helper factories ───────────────────────────────────────────────────── private static StorageViewModel CreateStorageViewModel() => new( Mock.Of(), Mock.Of(), NullLogger.Instance); private static PermissionsViewModel CreatePermissionsViewModel() => new( Mock.Of(), Mock.Of(), Mock.Of(), NullLogger.Instance); private static TransferViewModel CreateTransferViewModel() => new( Mock.Of(), Mock.Of(), new BulkResultCsvExportService(), NullLogger.Instance); private static MainWindowViewModel CreateMainWindowViewModel() { var tempFile = Path.GetTempFileName(); var profileRepo = new ProfileRepository(tempFile); var profileService = new ProfileService(profileRepo); var sessionManager = new SessionManager(new MsalClientFactory()); return new MainWindowViewModel( profileService, sessionManager, NullLogger.Instance); } private static IReadOnlyList TwoSites() => new List { new("https://contoso.sharepoint.com/sites/hr", "HR"), new("https://contoso.sharepoint.com/sites/finance", "Finance") }.AsReadOnly(); // ── Test 1: GlobalSitesChangedMessage carries site list ────────────────── [Fact] public void GlobalSitesChangedMessage_WhenSent_ReceiverGetsSites() { // Arrange IReadOnlyList? received = null; WeakReferenceMessenger.Default.Register( this, (_, m) => received = m.Value); var sites = TwoSites(); // Act WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert Assert.NotNull(received); Assert.Equal(2, received!.Count); Assert.Equal("https://contoso.sharepoint.com/sites/hr", received[0].Url); Assert.Equal("https://contoso.sharepoint.com/sites/finance", received[1].Url); } // ── Test 2: FeatureViewModelBase updates GlobalSites on message receive ── [Fact] public void FeatureViewModelBase_OnGlobalSitesChangedMessage_UpdatesGlobalSitesProperty() { // Arrange var vm = new TestFeatureViewModel(NullLogger.Instance); var sites = TwoSites(); // Act WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert Assert.Equal(2, vm.TestGlobalSites.Count); Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.TestGlobalSites[0].Url); Assert.Equal("https://contoso.sharepoint.com/sites/finance", vm.TestGlobalSites[1].Url); } // ── Test 3: StorageViewModel pre-fills SiteUrl from first global site ──── [Fact] public void OnGlobalSitesChanged_WithSites_PreFillsSiteUrlOnStorageTab() { // Arrange var vm = CreateStorageViewModel(); var sites = TwoSites(); // Act WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl); } // ── Test 4: StorageViewModel local override prevents global update ──────── [Fact] public void OnGlobalSitesChanged_AfterLocalSiteEntry_DoesNotOverrideSiteUrl() { // Arrange var vm = CreateStorageViewModel(); // User types a local URL — this sets the local override flag vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom"; // Act: global sites message arrives var sites = TwoSites(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert: local override takes precedence Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl); } // ── Test 5: StorageViewModel clearing SiteUrl reverts to global ────────── [Fact] public void OnSiteUrlChanged_WhenClearedAfterLocalOverride_RevertsToGlobalSite() { // Arrange: establish global sites first, then set a local override var vm = CreateStorageViewModel(); var sites = TwoSites(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Confirm global was applied Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl); // User types their own URL — sets local override vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom"; Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl); // Act: user clears the SiteUrl field vm.SiteUrl = string.Empty; // Assert: override is cleared and global site is re-applied Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl); } // ── Test 6: PermissionsViewModel pre-populates SelectedSites from global ─ [Fact] public void OnGlobalSitesChanged_WithSites_PrePopulatesSelectedSitesOnPermissionsTab() { // Arrange var vm = CreatePermissionsViewModel(); var sites = TwoSites(); // Act WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert Assert.Equal(2, vm.SelectedSites.Count); Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SelectedSites[0].Url); Assert.Equal("https://contoso.sharepoint.com/sites/finance", vm.SelectedSites[1].Url); } // ── Test 7: PermissionsViewModel local picker override prevents global ─── [Fact] public void OnGlobalSitesChanged_AfterLocalPickerOverride_DoesNotChangeSelectedSites() { // Arrange: add a site locally to simulate local site picker usage var vm = CreatePermissionsViewModel(); // Simulate what ExecuteOpenSitePicker does when user picks sites locally: // set _hasLocalSiteOverride = true (via reflection since it's private) and add a site. // We do this by using the TenantSwitchedMessage pattern to first let global sites // populate, then simulate a local override by directly reflecting the flag. var globalSites = TwoSites(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(globalSites)); // Global applied — SelectedSites has 2 entries Assert.Equal(2, vm.SelectedSites.Count); // Simulate local override: clear and add a local site, then set the override flag var localSite = new SiteInfo("https://contoso.sharepoint.com/sites/local", "Local"); vm.SelectedSites.Clear(); vm.SelectedSites.Add(localSite); // Use reflection to set the private _hasLocalSiteOverride flag (same as site picker would) var field = typeof(PermissionsViewModel) .GetField("_hasLocalSiteOverride", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); field!.SetValue(vm, true); // Act: new global sites arrive var newGlobalSites = new List { new("https://contoso.sharepoint.com/sites/new1", "New1") }.AsReadOnly(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(newGlobalSites)); // Assert: local override prevents global from replacing SelectedSites Assert.Single(vm.SelectedSites); Assert.Equal("https://contoso.sharepoint.com/sites/local", vm.SelectedSites[0].Url); } // ── Test 8: Tenant switch resets local override; new global sites are applied ─ [Fact] public void TenantSwitched_AfterLocalOverride_ResetsOverrideSoNewGlobalSitesAreApplied() { // Arrange: user has a local override (different from global) var vm = CreateStorageViewModel(); var sites = TwoSites(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Confirm global was applied Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl); // User types a different URL — sets local override vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom"; // New global sites message should NOT change SiteUrl because of local override var updatedGlobal = new List { new("https://contoso.sharepoint.com/sites/marketing", "Marketing") }.AsReadOnly(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(updatedGlobal)); Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl); // override still active // Act: tenant switch clears the override var newProfile = new TenantProfile { Name = "NewTenant", TenantUrl = "https://newtenant.sharepoint.com", ClientId = "new-client-id" }; WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(newProfile)); // Send new global sites after tenant switch — override should be gone var newSites = new List { new("https://newtenant.sharepoint.com/sites/sales", "Sales") }.AsReadOnly(); WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(newSites)); // Assert: new global site is applied (override was reset by tenant switch) Assert.Equal("https://newtenant.sharepoint.com/sites/sales", vm.SiteUrl); } // ── Test 9: TransferViewModel pre-fills SourceSiteUrl from first global ── [Fact] public void OnGlobalSitesChanged_WithSites_PreFillsSourceSiteUrlOnTransferTab() { // Arrange var vm = CreateTransferViewModel(); var sites = TwoSites(); // Act WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites)); // Assert: only SourceSiteUrl is pre-filled (first global site) Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SourceSiteUrl); } // ── Test 10: MainWindowViewModel GlobalSitesSelectedLabel updates with count [Fact] public void GlobalSitesSelectedLabel_WhenSitesAdded_ReflectsCount() { // Arrange var vm = CreateMainWindowViewModel(); // Initially no sites selected var initialLabel = vm.GlobalSitesSelectedLabel; Assert.DoesNotContain("1", initialLabel); // Should say "none" equivalent // Act: add two sites vm.GlobalSelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com/sites/hr", "HR")); vm.GlobalSelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com/sites/finance", "Finance")); // Assert: label reflects the count var label = vm.GlobalSitesSelectedLabel; Assert.Contains("2", label); // Ensure label is non-empty (different from the initial "none" state) Assert.NotEqual(initialLabel, label); } }