diff --git a/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs b/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
new file mode 100644
index 0000000..c1d8429
--- /dev/null
+++ b/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
@@ -0,0 +1,323 @@
+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);
+ }
+}