---
phase: 12-branding-ui-views
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs
  - SharepointToolbox/App.xaml
  - SharepointToolbox/Localization/Strings.resx
  - SharepointToolbox/Localization/Strings.fr.resx
  - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
  - SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs
  - SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs
autonomous: true
requirements:
  - BRAND-02
  - BRAND-04

must_haves:
  truths:
    - "Base64ToImageSourceConverter converts a data URI string to a non-null BitmapImage"
    - "Base64ToImageSourceConverter returns null for null, empty, or malformed input"
    - "Converter is registered in App.xaml as a global resource with key Base64ToImageConverter"
    - "ProfileManagementViewModel exposes ClientLogoPreview (string?) that updates when SelectedProfile changes, and after Browse/Clear/AutoPull commands"
    - "Localization keys for logo UI exist in both EN and FR resource files"
  artifacts:
    - path: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
      provides: "IValueConverter converting data URI strings to BitmapImage for WPF Image binding"
      contains: "class Base64ToImageSourceConverter"
    - path: "SharepointToolbox/App.xaml"
      provides: "Global converter registration"
      contains: "Base64ToImageConverter"
    - path: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
      provides: "ClientLogoPreview observable property for client logo display"
      contains: "ClientLogoPreview"
    - path: "SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs"
      provides: "Unit tests for converter behavior"
      min_lines: 30
  key_links:
    - from: "SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs"
      to: "SharepointToolbox/App.xaml"
      via: "resource registration"
      pattern: "Base64ToImageConverter"
    - from: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
      to: "SharepointToolbox/Core/Models/LogoData.cs"
      via: "data URI formatting"
      pattern: "ClientLogoPreview"
---

<objective>
Create the Base64ToImageSourceConverter, add localization keys for logo UI, register the converter globally, and add the ClientLogoPreview property to ProfileManagementViewModel.

Purpose: Provides the infrastructure (converter, localization, ViewModel property) that Plans 02 and 03 need to build the XAML views.

Output: Converter with tests, localization keys (EN+FR), App.xaml registration, ClientLogoPreview property with test coverage.
</objective>

<execution_context>
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-branding-ui-views/12-RESEARCH.md

<interfaces>
<!-- SettingsViewModel pattern for logo preview (reference for ProfileManagementViewModel) -->

From SharepointToolbox/ViewModels/Tabs/SettingsViewModel.cs:
```csharp
private string? _mspLogoPreview;
public string? MspLogoPreview
{
    get => _mspLogoPreview;
    private set { _mspLogoPreview = value; OnPropertyChanged(); }
}

// Set in LoadAsync:
var mspLogo = await _brandingService.GetMspLogoAsync();
MspLogoPreview = mspLogo is not null ? $"data:{mspLogo.MimeType};base64,{mspLogo.Base64}" : null;

// Set in BrowseMspLogoAsync:
MspLogoPreview = $"data:{logo.MimeType};base64,{logo.Base64}";

// Set in ClearMspLogoAsync:
MspLogoPreview = null;
```

From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs (current state):
```csharp
// BrowseClientLogoAsync sets SelectedProfile.ClientLogo = logo (LogoData)
// ClearClientLogoAsync sets SelectedProfile.ClientLogo = null
// AutoPullClientLogoAsync sets SelectedProfile.ClientLogo = logo
// NO ClientLogoPreview string property exists — this plan adds it
```

From SharepointToolbox/Core/Models/LogoData.cs:
```csharp
public record LogoData
{
    public string Base64 { get; init; } = string.Empty;
    public string MimeType { get; init; } = string.Empty;
}
```

From SharepointToolbox/Views/Converters/IndentConverter.cs (converter pattern):
```csharp
public class StringToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value is string s && !string.IsNullOrEmpty(s) ? Visibility.Visible : Visibility.Collapsed;
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        => throw new NotImplementedException();
}
```

From SharepointToolbox/App.xaml (converter registration pattern):
```xml
<conv:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create Base64ToImageSourceConverter with tests</name>
  <files>
    SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs,
    SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs
  </files>
  <behavior>
    - Test 1: Convert with null value returns null
    - Test 2: Convert with empty string returns null
    - Test 3: Convert with non-string value returns null
    - Test 4: Convert with valid data URI "data:image/png;base64,{validBase64}" returns a non-null BitmapImage
    - Test 5: Convert with malformed string (no "base64," prefix) returns null (does not throw)
    - Test 6: ConvertBack throws NotImplementedException
  </behavior>
  <action>
    1. Create `SharepointToolbox/Views/Converters/Base64ToImageSourceConverter.cs`:
       ```csharp
       using System.Globalization;
       using System.IO;
       using System.Windows.Data;
       using System.Windows.Media.Imaging;

       namespace SharepointToolbox.Views.Converters;

       public class Base64ToImageSourceConverter : IValueConverter
       {
           public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
           {
               if (value is not string dataUri || string.IsNullOrEmpty(dataUri))
                   return null;

               try
               {
                   var marker = "base64,";
                   var idx = dataUri.IndexOf(marker, StringComparison.Ordinal);
                   if (idx < 0) return null;

                   var base64 = dataUri[(idx + marker.Length)..];
                   var bytes = System.Convert.FromBase64String(base64);

                   var image = new BitmapImage();
                   using var ms = new MemoryStream(bytes);
                   image.BeginInit();
                   image.CacheOption = BitmapCacheOption.OnLoad;
                   image.StreamSource = ms;
                   image.EndInit();
                   image.Freeze();
                   return image;
               }
               catch
               {
                   return null;
               }
           }

           public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
               => throw new NotImplementedException();
       }
       ```
       Key decisions:
       - Parses data URI by finding "base64," marker — works with any mime type
       - `BitmapCacheOption.OnLoad` ensures the stream can be disposed immediately
       - `Freeze()` makes the image cross-thread safe (required for WPF binding)
       - Catches all exceptions to avoid binding errors — returns null on failure

    2. Create `SharepointToolbox.Tests/Converters/Base64ToImageSourceConverterTests.cs`:
       Write tests FIRST (RED), then verify GREEN.
       Use `[Trait("Category", "Unit")]` per project convention.
       Note: Tests that create BitmapImage need `[STAThread]` or run on STA thread. Use xUnit's `[WpfFact]` from `Xunit.StaFact` if available, or mark tests with `[Fact]` and handle STA requirement.
       For the valid data URI test, use a minimal valid 1x1 PNG base64: `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==`
       
       IMPORTANT: Check if `Xunit.StaFact` NuGet package is referenced in the test project. If not, the BitmapImage tests may need to be skipped or use a workaround (run converter logic that doesn't need STA for null/empty cases, skip the BitmapImage creation test if STA not available).
  </action>
  <verify>
    <automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter" --no-build -q</automated>
  </verify>
  <done>Converter class exists, handles all edge cases without throwing, tests pass.</done>
</task>

<task type="auto">
  <name>Task 2: Register converter in App.xaml</name>
  <files>
    SharepointToolbox/App.xaml
  </files>
  <behavior>
    - App.xaml contains a Base64ToImageSourceConverter resource with key "Base64ToImageConverter"
  </behavior>
  <action>
    1. In `SharepointToolbox/App.xaml`, add inside `<Application.Resources>`:
       ```xml
       <conv:Base64ToImageSourceConverter x:Key="Base64ToImageConverter" />
       ```
       Place it after the existing converter registrations.
  </action>
  <verify>
    <automated>dotnet build --no-restore -warnaserror</automated>
  </verify>
  <done>Converter is globally available via StaticResource Base64ToImageConverter.</done>
</task>

<task type="auto">
  <name>Task 3: Add localization keys for logo UI (EN + FR)</name>
  <files>
    SharepointToolbox/Localization/Strings.resx,
    SharepointToolbox/Localization/Strings.fr.resx
  </files>
  <behavior>
    - Both resx files contain matching keys for logo UI labels
  </behavior>
  <action>
    1. Add to `Strings.resx` (EN):
       - `settings.logo.title` = "MSP Logo"
       - `settings.logo.browse` = "Import"
       - `settings.logo.clear` = "Clear"
       - `settings.logo.nopreview` = "No logo configured"
       - `profile.logo.title` = "Client Logo"
       - `profile.logo.browse` = "Import"
       - `profile.logo.clear` = "Clear"
       - `profile.logo.autopull` = "Pull from Entra"
       - `profile.logo.nopreview` = "No logo configured"

    2. Add to `Strings.fr.resx` (FR):
       - `settings.logo.title` = "Logo MSP"
       - `settings.logo.browse` = "Importer"
       - `settings.logo.clear` = "Effacer"
       - `settings.logo.nopreview` = "Aucun logo configuré"
       - `profile.logo.title` = "Logo client"
       - `profile.logo.browse` = "Importer"
       - `profile.logo.clear` = "Effacer"
       - `profile.logo.autopull` = "Importer depuis Entra"
       - `profile.logo.nopreview` = "Aucun logo configuré"
  </action>
  <verify>
    <automated>dotnet build --no-restore -warnaserror</automated>
  </verify>
  <done>All 9 localization keys exist in both EN and FR resource files.</done>
</task>

<task type="auto">
  <name>Task 4: Add ClientLogoPreview property to ProfileManagementViewModel</name>
  <files>
    SharepointToolbox/ViewModels/ProfileManagementViewModel.cs,
    SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelLogoTests.cs
  </files>
  <behavior>
    - ProfileManagementViewModel exposes ClientLogoPreview (string?) property
    - ClientLogoPreview updates to data URI when SelectedProfile changes and has a ClientLogo
    - ClientLogoPreview updates to null when SelectedProfile is null or has no ClientLogo
    - BrowseClientLogoAsync updates ClientLogoPreview after successful import
    - ClearClientLogoAsync sets ClientLogoPreview to null
    - AutoPullClientLogoAsync updates ClientLogoPreview after successful pull
  </behavior>
  <action>
    1. Add to `ProfileManagementViewModel.cs`:
       ```csharp
       private string? _clientLogoPreview;
       public string? ClientLogoPreview
       {
           get => _clientLogoPreview;
           private set { _clientLogoPreview = value; OnPropertyChanged(); }
       }

       private static string? FormatLogoPreview(LogoData? logo)
           => logo is not null ? $"data:{logo.MimeType};base64,{logo.Base64}" : null;
       ```

    2. Update `OnSelectedProfileChanged` to refresh preview:
       ```csharp
       partial void OnSelectedProfileChanged(TenantProfile? value)
       {
           ClientLogoPreview = FormatLogoPreview(value?.ClientLogo);
           // ... existing NotifyCanExecuteChanged calls ...
       }
       ```

    3. Update `BrowseClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add:
       ```csharp
       ClientLogoPreview = FormatLogoPreview(logo);
       ```

    4. Update `ClearClientLogoAsync` — after `SelectedProfile.ClientLogo = null;` add:
       ```csharp
       ClientLogoPreview = null;
       ```

    5. Update `AutoPullClientLogoAsync` — after `SelectedProfile.ClientLogo = logo;` add:
       ```csharp
       ClientLogoPreview = FormatLogoPreview(logo);
       ```

    6. Update existing tests in `ProfileManagementViewModelLogoTests.cs`:
       - Add test: ClientLogoPreview is null when no profile selected
       - Add test: ClientLogoPreview updates when SelectedProfile with logo is selected
       - Add test: ClearClientLogoAsync sets ClientLogoPreview to null
  </action>
  <verify>
    <automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModel" --no-build -q</automated>
  </verify>
  <done>ClientLogoPreview property exists and stays in sync with SelectedProfile.ClientLogo across all mutations. Tests pass.</done>
</task>

</tasks>

<verification>
```bash
dotnet build --no-restore -warnaserror
dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~Base64ToImageSourceConverter|FullyQualifiedName~ProfileManagementViewModel" --no-build -q
```
Both commands must pass with zero failures.
</verification>

<success_criteria>
- Base64ToImageSourceConverter converts data URI strings to BitmapImage, returns null on bad input
- Converter registered in App.xaml as "Base64ToImageConverter"
- 9 localization keys present in both Strings.resx and Strings.fr.resx
- ProfileManagementViewModel.ClientLogoPreview stays in sync with SelectedProfile.ClientLogo
- All tests pass, build succeeds with zero warnings
</success_criteria>

<output>
After completion, create `.planning/phases/12-branding-ui-views/12-01-SUMMARY.md`
</output>
