using System.Collections.ObjectModel; using System.Reflection; using CommunityToolkit.Mvvm.Messaging; using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using SharepointToolbox.Core.Messages; using SharepointToolbox.Core.Models; using SharepointToolbox.Services; using SharepointToolbox.ViewModels; using SharepointToolbox.ViewModels.Tabs; namespace SharepointToolbox.Tests.ViewModels; /// /// Unit tests for StorageViewModel chart functionality (Phase 09 Plan 04). /// Verifies: chart series from metrics, bar series structure, donut/bar toggle, /// top-10 + Other aggregation, no-Other for <=10, tenant switch cleanup, empty data. /// Uses reflection to set FileTypeMetrics directly, bypassing ClientContext dependency. /// public class StorageViewModelChartTests { public StorageViewModelChartTests() { WeakReferenceMessenger.Default.Reset(); } // -- Helper factories -------------------------------------------------------- private static StorageViewModel CreateViewModel() { var mockStorage = new Mock(); var mockSession = new Mock(); var vm = new StorageViewModel( mockStorage.Object, mockSession.Object, NullLogger.Instance); vm.SetCurrentProfile(new TenantProfile { Name = "Test", TenantUrl = "https://test.sharepoint.com", ClientId = "test-id" }); return vm; } /// /// Sets FileTypeMetrics via the property (private setter) using reflection, /// which also triggers UpdateChartSeries. /// private static void SetFileTypeMetrics(StorageViewModel vm, IList metrics) { var prop = typeof(StorageViewModel).GetProperty( nameof(StorageViewModel.FileTypeMetrics), BindingFlags.Public | BindingFlags.Instance); prop!.SetValue(vm, new ObservableCollection(metrics)); } private static List MakeMetrics(int count) { var extensions = new[] { ".docx", ".pdf", ".xlsx", ".pptx", ".jpg", ".png", ".mp4", ".zip", ".csv", ".html", ".txt", ".json", ".xml", ".msg", ".eml" }; var metrics = new List(); for (int i = 0; i < count; i++) { string ext = i < extensions.Length ? extensions[i] : $".ext{i}"; metrics.Add(new FileTypeMetric(ext, (count - i) * 1024L * 1024, (count - i) * 10)); } return metrics; } // -- Test 1: Chart series populated from metrics ----------------------------- [Fact] public void After_setting_metrics_HasChartData_is_true_and_PieChartSeries_has_entries() { var vm = CreateViewModel(); var metrics = MakeMetrics(5); SetFileTypeMetrics(vm, metrics); Assert.True(vm.HasChartData); Assert.NotEmpty(vm.PieChartSeries); Assert.Equal(5, vm.PieChartSeries.Count()); } // -- Test 2: Bar series has one ColumnSeries with correct value count -------- [Fact] public void After_setting_metrics_BarChartSeries_has_one_ColumnSeries_with_matching_values() { var vm = CreateViewModel(); var metrics = MakeMetrics(5); SetFileTypeMetrics(vm, metrics); var barSeries = vm.BarChartSeries.ToList(); Assert.Single(barSeries); var columnSeries = Assert.IsType>(barSeries[0]); Assert.Equal(5, columnSeries.Values!.Count()); } // -- Test 3: Toggle IsDonutChart changes PieChartSeries InnerRadius ---------- [Fact] public void Toggle_IsDonutChart_changes_PieChartSeries_InnerRadius() { var vm = CreateViewModel(); var metrics = MakeMetrics(3); SetFileTypeMetrics(vm, metrics); // Initially IsDonutChart=true => InnerRadius=50 var pieBefore = vm.PieChartSeries.Cast>().ToList(); Assert.All(pieBefore, s => Assert.Equal(50, s.InnerRadius)); // Toggle to bar (not donut) => InnerRadius=0 vm.IsDonutChart = false; var pieAfter = vm.PieChartSeries.Cast>().ToList(); Assert.All(pieAfter, s => Assert.Equal(0, s.InnerRadius)); } // -- Test 4: More than 10 file types => 11 entries (10 + Other) -------------- [Fact] public void More_than_10_metrics_produces_11_series_entries_with_Other() { var vm = CreateViewModel(); var metrics = MakeMetrics(15); SetFileTypeMetrics(vm, metrics); // Pie series: 10 real + 1 "Other" = 11 Assert.Equal(11, vm.PieChartSeries.Count()); // Last pie entry should be named "OTHER" (DisplayLabel uppercases extension) var lastPie = vm.PieChartSeries.Last(); Assert.Equal("OTHER", lastPie.Name); // Bar series column should have 11 values var columnSeries = Assert.IsType>(vm.BarChartSeries.First()); Assert.Equal(11, columnSeries.Values!.Count()); // X-axis should have 11 labels Assert.Equal(11, vm.BarXAxes[0].Labels!.Count); } // -- Test 5: 10 or fewer file types => no "Other" entry ---------------------- [Fact] public void Ten_or_fewer_metrics_produces_no_Other_entry() { var vm = CreateViewModel(); var metrics = MakeMetrics(10); SetFileTypeMetrics(vm, metrics); Assert.Equal(10, vm.PieChartSeries.Count()); // No entry named "OTHER" (DisplayLabel uppercases) Assert.DoesNotContain(vm.PieChartSeries, s => s.Name == "OTHER"); } // -- Test 6: Tenant switch clears chart data --------------------------------- [Fact] public void OnTenantSwitched_clears_FileTypeMetrics_and_HasChartData_is_false() { var vm = CreateViewModel(); var metrics = MakeMetrics(5); SetFileTypeMetrics(vm, metrics); Assert.True(vm.HasChartData); // Act: send TenantSwitchedMessage var newProfile = new TenantProfile { Name = "NewTenant", TenantUrl = "https://newtenant.sharepoint.com", ClientId = "new-id" }; WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(newProfile)); Assert.False(vm.HasChartData); Assert.Empty(vm.FileTypeMetrics); Assert.Empty(vm.PieChartSeries); Assert.Empty(vm.BarChartSeries); } // -- Test 7: Empty metrics => HasChartData false, series empty --------------- [Fact] public void Empty_metrics_yields_HasChartData_false_and_empty_series() { var vm = CreateViewModel(); SetFileTypeMetrics(vm, new List()); Assert.False(vm.HasChartData); Assert.Empty(vm.PieChartSeries); Assert.Empty(vm.BarChartSeries); Assert.Empty(vm.BarXAxes); Assert.Empty(vm.BarYAxes); } }