---
phase: 19-app-registration-removal
plan: 02
type: execute
wave: 2
depends_on: ["19-01"]
files_modified:
  - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs
  - SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml
  - SharepointToolbox/Localization/Strings.resx
  - SharepointToolbox/Localization/Strings.fr.resx
  - SharepointToolbox/App.xaml.cs
  - SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs
autonomous: true
requirements: [APPREG-01, APPREG-04, APPREG-05]

must_haves:
  truths:
    - "Register App button visible in profile dialog when a profile is selected and has no AppId"
    - "Remove App button visible when selected profile has a non-null AppId"
    - "Clicking Register checks Global Admin first; if not admin, shows fallback instructions panel"
    - "Clicking Register when admin runs full registration and stores AppId on profile"
    - "Clicking Remove deletes the app registration and clears AppId + MSAL session"
    - "Fallback panel shows step-by-step manual registration instructions"
    - "Status feedback shown during registration/removal (busy indicator + result message)"
    - "All strings localized in EN and FR"
  artifacts:
    - path: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
      provides: "RegisterAppCommand, RemoveAppCommand, status/fallback properties"
      contains: "RegisterAppCommand"
    - path: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
      provides: "Register/Remove buttons, fallback instructions panel, status area"
      contains: "RegisterAppCommand"
    - path: "SharepointToolbox/Localization/Strings.resx"
      provides: "EN localization for register/remove/fallback strings"
      contains: "profile.register"
    - path: "SharepointToolbox/Localization/Strings.fr.resx"
      provides: "FR localization for register/remove/fallback strings"
      contains: "profile.register"
    - path: "SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs"
      provides: "Unit tests for register/remove commands"
      min_lines: 50
  key_links:
    - from: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs"
      to: "IAppRegistrationService"
      via: "constructor injection"
      pattern: "IAppRegistrationService"
    - from: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml"
      to: "ProfileManagementViewModel"
      via: "data binding"
      pattern: "RegisterAppCommand"
    - from: "SharepointToolbox/App.xaml.cs"
      to: "AppRegistrationService"
      via: "DI registration"
      pattern: "IAppRegistrationService"
---

<objective>
Wire RegisterApp and RemoveApp commands into the ProfileManagementViewModel and dialog XAML, with fallback instructions panel and full EN/FR localization.

Purpose: This is the user-facing layer — the profile dialog becomes the entry point for tenant onboarding via app registration, with a guided fallback when permissions are insufficient.
Output: Working register/remove UI in profile dialog, localized strings, DI wiring, ViewModel unit tests
</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/19-app-registration-removal/19-RESEARCH.md
@.planning/phases/19-app-registration-removal/19-01-SUMMARY.md

<interfaces>
<!-- From Plan 01 outputs -->

From SharepointToolbox/Services/IAppRegistrationService.cs:
```csharp
public interface IAppRegistrationService
{
    Task<bool> IsGlobalAdminAsync(string clientId, CancellationToken ct);
    Task<AppRegistrationResult> RegisterAsync(string clientId, string tenantDisplayName, CancellationToken ct);
    Task RemoveAsync(string clientId, string appId, CancellationToken ct);
    Task ClearMsalSessionAsync(string clientId, string tenantUrl);
}
```

From SharepointToolbox/Core/Models/AppRegistrationResult.cs:
```csharp
public class AppRegistrationResult
{
    public bool IsSuccess { get; }
    public bool IsFallback { get; }
    public string? AppId { get; }
    public string? ErrorMessage { get; }
    public static AppRegistrationResult Success(string appId);
    public static AppRegistrationResult Failure(string errorMessage);
    public static AppRegistrationResult FallbackRequired();
}
```

From SharepointToolbox/Core/Models/TenantProfile.cs:
```csharp
public class TenantProfile
{
    public string Name { get; set; } = string.Empty;
    public string TenantUrl { get; set; } = string.Empty;
    public string ClientId { get; set; } = string.Empty;
    public LogoData? ClientLogo { get; set; }
    public string? AppId { get; set; }  // NEW in Plan 01
}
```

From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs (existing):
```csharp
public partial class ProfileManagementViewModel : ObservableObject
{
    // Constructor: ProfileService, IBrandingService, GraphClientFactory, ILogger
    // Commands: AddCommand, RenameCommand, DeleteCommand, BrowseClientLogoCommand, ClearClientLogoCommand, AutoPullClientLogoCommand
    // Properties: SelectedProfile, NewName, NewTenantUrl, NewClientId, ValidationMessage, ClientLogoPreview, Profiles
}
```

From SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml (existing):
- 5-row Grid: profiles list, input fields, logo section, buttons
- Window size: 500x620
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: ViewModel commands + DI + Localization</name>
  <files>
    SharepointToolbox/ViewModels/ProfileManagementViewModel.cs,
    SharepointToolbox/App.xaml.cs,
    SharepointToolbox/Localization/Strings.resx,
    SharepointToolbox/Localization/Strings.fr.resx
  </files>
  <action>
    1. **Update ProfileManagementViewModel constructor** to accept `IAppRegistrationService` as new last parameter. Store as `_appRegistrationService` field.

    2. **Add observable properties:**
       - `[ObservableProperty] private bool _isRegistering;` — true during async registration/removal
       - `[ObservableProperty] private bool _showFallbackInstructions;` — true when fallback panel should be visible
       - `[ObservableProperty] private string _registrationStatus = string.Empty;` — status text for user feedback
       - Add a computed `HasRegisteredApp` property: `public bool HasRegisteredApp => SelectedProfile?.AppId != null;`
       - In `OnSelectedProfileChanged`, call `OnPropertyChanged(nameof(HasRegisteredApp))` and notify register/remove commands

    3. **Add commands:**
       - `public IAsyncRelayCommand RegisterAppCommand { get; }` — initialized in constructor as `new AsyncRelayCommand(RegisterAppAsync, CanRegisterApp)`
       - `public IAsyncRelayCommand RemoveAppCommand { get; }` — initialized as `new AsyncRelayCommand(RemoveAppAsync, CanRemoveApp)`
       - `CanRegisterApp()`: `SelectedProfile != null && SelectedProfile.AppId == null && !IsRegistering`
       - `CanRemoveApp()`: `SelectedProfile != null && SelectedProfile.AppId != null && !IsRegistering`

    4. **RegisterAppAsync implementation:**
       ```
       a. Set IsRegistering = true, ShowFallbackInstructions = false, RegistrationStatus = localized "Checking permissions..."
       b. Call IsGlobalAdminAsync(SelectedProfile.ClientId, ct)
       c. If not admin: ShowFallbackInstructions = true, RegistrationStatus = localized "Insufficient permissions", IsRegistering = false, return
       d. RegistrationStatus = localized "Registering application..."
       e. Call RegisterAsync(SelectedProfile.ClientId, SelectedProfile.Name, ct)
       f. If result.IsSuccess: SelectedProfile.AppId = result.AppId, save profile via _profileService.UpdateProfileAsync, RegistrationStatus = localized "Registration successful", OnPropertyChanged(nameof(HasRegisteredApp))
       g. If result.IsFallback or !IsSuccess: RegistrationStatus = result.ErrorMessage ?? localized "Registration failed"
       h. Finally: IsRegistering = false, notify command CanExecute
       ```

    5. **RemoveAppAsync implementation:**
       ```
       a. Set IsRegistering = true, RegistrationStatus = localized "Removing application..."
       b. Call RemoveAsync(SelectedProfile.ClientId, SelectedProfile.AppId!, ct)
       c. Call ClearMsalSessionAsync(SelectedProfile.ClientId, SelectedProfile.TenantUrl)
       d. SelectedProfile.AppId = null, save profile, RegistrationStatus = localized "Application removed", OnPropertyChanged(nameof(HasRegisteredApp))
       e. Finally: IsRegistering = false, notify command CanExecute
       f. Wrap in try/catch, log errors, show error in RegistrationStatus
       ```

    6. **Partial method for IsRegistering changes:** Add `partial void OnIsRegisteringChanged(bool value)` that calls `RegisterAppCommand.NotifyCanExecuteChanged()` and `RemoveAppCommand.NotifyCanExecuteChanged()`.

    7. **Update App.xaml.cs DI registration:**
       - Register `services.AddSingleton<IAppRegistrationService, AppRegistrationService>();`
       - Update `ProfileManagementViewModel` transient registration (it already resolves from DI, the new constructor param will be injected automatically)

    8. **Add localization strings to Strings.resx (EN):**
       - `profile.register` = "Register App"
       - `profile.remove` = "Remove App"
       - `profile.register.checking` = "Checking permissions..."
       - `profile.register.registering` = "Registering application..."
       - `profile.register.success` = "Application registered successfully"
       - `profile.register.failed` = "Registration failed"
       - `profile.register.noperm` = "Insufficient permissions for automatic registration"
       - `profile.remove.removing` = "Removing application..."
       - `profile.remove.success` = "Application removed successfully"
       - `profile.fallback.title` = "Manual Registration Required"
       - `profile.fallback.step1` = "1. Go to Azure Portal > App registrations > New registration"
       - `profile.fallback.step2` = "2. Name: 'SharePoint Toolbox - {0}', Supported account types: Single tenant"
       - `profile.fallback.step3` = "3. Redirect URI: Public client, https://login.microsoftonline.com/common/oauth2/nativeclient"
       - `profile.fallback.step4` = "4. Under API permissions, add: Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) and SharePoint (AllSites.FullControl)"
       - `profile.fallback.step5` = "5. Grant admin consent for all permissions"
       - `profile.fallback.step6` = "6. Copy the Application (client) ID and paste it in the Client ID field above"

    9. **Add localization strings to Strings.fr.resx (FR):**
       - `profile.register` = "Enregistrer l'app"
       - `profile.remove` = "Supprimer l'app"
       - `profile.register.checking` = "Verification des permissions..."
       - `profile.register.registering` = "Enregistrement de l'application..."
       - `profile.register.success` = "Application enregistree avec succes"
       - `profile.register.failed` = "L'enregistrement a echoue"
       - `profile.register.noperm` = "Permissions insuffisantes pour l'enregistrement automatique"
       - `profile.remove.removing` = "Suppression de l'application..."
       - `profile.remove.success` = "Application supprimee avec succes"
       - `profile.fallback.title` = "Enregistrement manuel requis"
       - `profile.fallback.step1` = "1. Allez dans le portail Azure > Inscriptions d'applications > Nouvelle inscription"
       - `profile.fallback.step2` = "2. Nom: 'SharePoint Toolbox - {0}', Types de comptes: Locataire unique"
       - `profile.fallback.step3` = "3. URI de redirection: Client public, https://login.microsoftonline.com/common/oauth2/nativeclient"
       - `profile.fallback.step4` = "4. Sous Permissions API, ajouter: Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) et SharePoint (AllSites.FullControl)"
       - `profile.fallback.step5` = "5. Accorder le consentement administrateur pour toutes les permissions"
       - `profile.fallback.step6` = "6. Copier l'ID d'application (client) et le coller dans le champ ID Client ci-dessus"

    Use proper accented characters in FR strings (e with accents etc.).
  </action>
  <verify>
    <automated>dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore 2>&1 | tail -5</automated>
  </verify>
  <done>ProfileManagementViewModel has RegisterAppCommand and RemoveAppCommand, DI wired, all localization strings in EN and FR, solution builds clean</done>
</task>

<task type="auto">
  <name>Task 2: Profile dialog XAML + ViewModel tests</name>
  <files>
    SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml,
    SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs
  </files>
  <action>
    1. **Update ProfileManagementDialog.xaml:**
       - Increase window Height from 620 to 750 to accommodate new section
       - Add a new row (insert as Row 4, shift existing buttons to Row 5) with an "App Registration" section:

       ```xml
       <!-- App Registration -->
       <StackPanel Grid.Row="4" Margin="0,8,0,8">
         <Label Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.title]}"
                FontWeight="SemiBold" Padding="0,0,0,4"
                Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}" />

         <!-- Register / Remove buttons -->
         <StackPanel Orientation="Horizontal" Margin="0,0,0,4">
           <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.register]}"
                   Command="{Binding RegisterAppCommand}" Width="120" Margin="0,0,8,0" />
           <Button Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.remove]}"
                   Command="{Binding RemoveAppCommand}" Width="120" Margin="0,0,8,0" />
         </StackPanel>

         <!-- Status text -->
         <TextBlock Text="{Binding RegistrationStatus}" FontSize="11" Margin="0,2,0,0"
                    Foreground="#006600"
                    Visibility="{Binding RegistrationStatus, Converter={StaticResource StringToVisibilityConverter}}" />

         <!-- Fallback instructions panel -->
         <Border BorderBrush="#DDDDDD" BorderThickness="1" Padding="8" CornerRadius="4" Margin="0,4,0,0"
                 Visibility="{Binding ShowFallbackInstructions, Converter={StaticResource BooleanToVisibilityConverter}}">
           <StackPanel>
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step1]}"
                        TextWrapping="Wrap" Margin="0,2" />
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step2]}"
                        TextWrapping="Wrap" Margin="0,2" />
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step3]}"
                        TextWrapping="Wrap" Margin="0,2" />
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step4]}"
                        TextWrapping="Wrap" Margin="0,2" />
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step5]}"
                        TextWrapping="Wrap" Margin="0,2" />
             <TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[profile.fallback.step6]}"
                        TextWrapping="Wrap" Margin="0,2" />
           </StackPanel>
         </Border>
       </StackPanel>
       ```

       - Check if `BooleanToVisibilityConverter` already exists in the project resources. If not, use a Style with DataTrigger instead (matching Phase 18 pattern of DataTrigger-based visibility). Alternatively, WPF has `BooleanToVisibilityConverter` built-in — add `<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>` to Window.Resources if not already present.
       - Update Row 5 (buttons row) Grid.Row from 4 to 5
       - Add a 6th RowDefinition if needed (Auto for app registration, Auto for buttons)

    2. **Create ProfileManagementViewModelRegistrationTests.cs:**
       - Mock `IAppRegistrationService`, `ProfileService`, `IBrandingService`, `GraphClientFactory`, `ILogger`
       - Use the same mocking library as existing VM tests in the project

       Tests:
       - `RegisterAppCommand_CanExecute_WhenProfileSelected_AndNoAppId`: set SelectedProfile with null AppId, verify CanExecute = true
       - `RegisterAppCommand_CannotExecute_WhenNoProfile`: verify CanExecute = false
       - `RemoveAppCommand_CanExecute_WhenProfileHasAppId`: set SelectedProfile with non-null AppId, verify CanExecute = true
       - `RemoveAppCommand_CannotExecute_WhenNoAppId`: set SelectedProfile with null AppId, verify CanExecute = false
       - `RegisterApp_ShowsFallback_WhenNotAdmin`: mock IsGlobalAdminAsync to return false, execute command, verify ShowFallbackInstructions = true
       - `RegisterApp_SetsAppId_OnSuccess`: mock IsGlobalAdminAsync true + RegisterAsync Success, verify SelectedProfile.AppId set
       - `RemoveApp_ClearsAppId`: mock RemoveAsync + ClearMsalSessionAsync, verify SelectedProfile.AppId = null

       All tests `[Trait("Category", "Unit")]`.
  </action>
  <verify>
    <automated>dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModelRegistrationTests" --no-restore --verbosity normal 2>&1 | tail -20</automated>
  </verify>
  <done>Profile dialog shows Register/Remove buttons with correct visibility, fallback instructions panel toggles on ShowFallbackInstructions, all 7 ViewModel tests pass, solution builds clean</done>
</task>

</tasks>

<verification>
1. `dotnet build` — full solution compiles
2. `dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~ProfileManagementViewModelRegistrationTests"` — all VM tests green
3. XAML renders Register/Remove buttons in the dialog
4. Fallback panel visibility bound to ShowFallbackInstructions
5. Localization strings exist in both Strings.resx and Strings.fr.resx
</verification>

<success_criteria>
- Register App button visible when profile selected with no AppId
- Remove App button visible when profile has AppId
- Fallback instructions panel appears when IsGlobalAdmin returns false
- All strings localized in EN and FR
- All ViewModel tests pass
- DI wiring complete in App.xaml.cs
</success_criteria>

<output>
After completion, create `.planning/phases/19-app-registration-removal/19-02-SUMMARY.md`
</output>
