---
phase: 01-foundation
plan: 03
type: execute
wave: 3
depends_on:
  - 01-01
  - 01-02
files_modified:
  - SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs
  - SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs
  - SharepointToolbox/Services/ProfileService.cs
  - SharepointToolbox/Services/SettingsService.cs
  - SharepointToolbox.Tests/Services/ProfileServiceTests.cs
  - SharepointToolbox.Tests/Services/SettingsServiceTests.cs
autonomous: true
requirements:
  - FOUND-02
  - FOUND-10
  - FOUND-12
must_haves:
  truths:
    - "ProfileService reads Sharepoint_Export_profiles.json without migration — field names are the contract"
    - "SettingsService reads Sharepoint_Settings.json preserving dataFolder and lang fields"
    - "Write operations use write-then-replace (file.tmp → validate → File.Move) with SemaphoreSlim(1)"
    - "ProfileService unit tests: SaveAndLoad round-trips, corrupt file recovery, concurrent write safety"
    - "SettingsService unit tests: SaveAndLoad round-trips, default settings when file missing"
  artifacts:
    - path: "SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs"
      provides: "File I/O for profiles JSON with write-then-replace"
      contains: "SemaphoreSlim"
    - path: "SharepointToolbox/Services/ProfileService.cs"
      provides: "CRUD operations on TenantProfile collection"
      exports: ["AddProfile", "RenameProfile", "DeleteProfile", "GetProfiles"]
    - path: "SharepointToolbox/Services/SettingsService.cs"
      provides: "Read/write for app settings including data folder and language"
      exports: ["GetSettings", "SaveSettings"]
    - path: "SharepointToolbox.Tests/Services/ProfileServiceTests.cs"
      provides: "Unit tests covering FOUND-02 and FOUND-10"
  key_links:
    - from: "SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs"
      to: "Sharepoint_Export_profiles.json"
      via: "System.Text.Json deserialization of { profiles: [...] } wrapper"
      pattern: "profiles"
    - from: "SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs"
      to: "Sharepoint_Settings.json"
      via: "System.Text.Json deserialization of { dataFolder, lang }"
      pattern: "dataFolder"
---

<objective>
Build the persistence layer: ProfileRepository and SettingsRepository (Infrastructure) plus ProfileService and SettingsService (Services layer). Implement write-then-replace safety. Write unit tests that validate the round-trip and edge cases.

Purpose: Profiles and settings are the first user-visible data. Corrupt files or wrong field names would break existing users' data on migration. Unit tests lock in the JSON schema contract.
Output: 4 production files + 2 test files with passing unit tests.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-02-SUMMARY.md

<interfaces>
<!-- From Core/Models/TenantProfile.cs (plan 01-02) -->
```csharp
namespace SharepointToolbox.Core.Models;
public class TenantProfile
{
    public string Name { get; set; } = string.Empty;
    public string TenantUrl { get; set; } = string.Empty;
    public string ClientId { get; set; } = string.Empty;
}
```

<!-- JSON schema contracts (live user data — field names are frozen) -->
// Sharepoint_Export_profiles.json
{ "profiles": [{ "name": "...", "tenantUrl": "...", "clientId": "..." }] }

// Sharepoint_Settings.json
{ "dataFolder": "...", "lang": "en" }
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: ProfileRepository and ProfileService with write-then-replace</name>
  <files>
    SharepointToolbox/Infrastructure/Persistence/ProfileRepository.cs,
    SharepointToolbox/Services/ProfileService.cs,
    SharepointToolbox.Tests/Services/ProfileServiceTests.cs
  </files>
  <behavior>
    - Test: SaveAsync then LoadAsync round-trips a list of TenantProfiles with correct field values
    - Test: LoadAsync on missing file returns empty list (no exception)
    - Test: LoadAsync on corrupt JSON throws InvalidDataException (not silently returns empty)
    - Test: Concurrent SaveAsync calls don't corrupt the file (SemaphoreSlim ensures ordering)
    - Test: ProfileService.AddProfile assigns the new profile and persists immediately
    - Test: ProfileService.RenameProfile changes Name, persists, throws if profile not found
    - Test: ProfileService.DeleteProfile removes by Name, throws if not found
    - Test: Saved JSON wraps profiles in { "profiles": [...] } root object (schema compatibility)
  </behavior>
  <action>
    Create `Infrastructure/Persistence/` and `Services/` directories.

    **ProfileRepository.cs** — handles raw file I/O:
    ```csharp
    namespace SharepointToolbox.Infrastructure.Persistence;

    public class ProfileRepository
    {
        private readonly string _filePath;
        private readonly SemaphoreSlim _writeLock = new(1, 1);

        public ProfileRepository(string filePath)
        {
            _filePath = filePath;
        }

        public async Task<IReadOnlyList<TenantProfile>> LoadAsync()
        {
            if (!File.Exists(_filePath))
                return Array.Empty<TenantProfile>();

            var json = await File.ReadAllTextAsync(_filePath, Encoding.UTF8);
            var root = JsonSerializer.Deserialize<ProfilesRoot>(json,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
            return root?.Profiles ?? Array.Empty<TenantProfile>();
        }

        public async Task SaveAsync(IReadOnlyList<TenantProfile> profiles)
        {
            await _writeLock.WaitAsync();
            try
            {
                var root = new ProfilesRoot { Profiles = profiles.ToList() };
                var json = JsonSerializer.Serialize(root,
                    new JsonSerializerOptions { WriteIndented = true,
                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
                var tmpPath = _filePath + ".tmp";
                Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
                await File.WriteAllTextAsync(tmpPath, json, Encoding.UTF8);
                // Validate round-trip before replacing
                JsonDocument.Parse(await File.ReadAllTextAsync(tmpPath)).Dispose();
                File.Move(tmpPath, _filePath, overwrite: true);
            }
            finally { _writeLock.Release(); }
        }

        private sealed class ProfilesRoot
        {
            public List<TenantProfile> Profiles { get; set; } = new();
        }
    }
    ```
    Note: Use `PropertyNamingPolicy = JsonNamingPolicy.CamelCase` to serialize `Name`→`name`, `TenantUrl`→`tenantUrl`, `ClientId`→`clientId` matching the existing JSON schema.

    **ProfileService.cs** — CRUD on top of repository:
    - Constructor takes `ProfileRepository` (inject via DI later; for now accept in constructor)
    - `Task<IReadOnlyList<TenantProfile>> GetProfilesAsync()`
    - `Task AddProfileAsync(TenantProfile profile)` — validates Name not empty, TenantUrl valid URL, ClientId not empty; throws `ArgumentException` for invalid inputs
    - `Task RenameProfileAsync(string existingName, string newName)` — throws `KeyNotFoundException` if not found
    - `Task DeleteProfileAsync(string name)` — throws `KeyNotFoundException` if not found
    - All mutations load → modify in-memory list → save (single-load-modify-save to preserve order)

    **ProfileServiceTests.cs** — Replace the stub with real tests using temp file paths:
    ```csharp
    public class ProfileServiceTests : IDisposable
    {
        private readonly string _tempFile = Path.GetTempFileName();
        // Dispose deletes temp file

        [Fact]
        public async Task SaveAndLoad_RoundTrips_Profiles() { ... }
        // etc.
    }
    ```
    Tests must use a temp file, not the real user data file. All tests in `[Trait("Category", "Unit")]`.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~ProfileServiceTests" 2>&1 | tail -10</automated>
  </verify>
  <done>All ProfileServiceTests pass (no skips). JSON output uses camelCase field names matching existing schema. Write-then-replace with SemaphoreSlim implemented.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: SettingsRepository and SettingsService</name>
  <files>
    SharepointToolbox/Infrastructure/Persistence/SettingsRepository.cs,
    SharepointToolbox/Services/SettingsService.cs,
    SharepointToolbox.Tests/Services/SettingsServiceTests.cs
  </files>
  <behavior>
    - Test: LoadAsync returns default settings (dataFolder = empty string, lang = "en") when file missing
    - Test: SaveAsync then LoadAsync round-trips dataFolder and lang values exactly
    - Test: Serialized JSON contains "dataFolder" and "lang" keys (not DataFolder/Lang — schema compatibility)
    - Test: SaveAsync uses write-then-replace (tmp file created, then moved)
    - Test: SettingsService.SetLanguageAsync("fr") persists lang="fr"
    - Test: SettingsService.SetDataFolderAsync("C:\\Exports") persists dataFolder path
  </behavior>
  <action>
    **AppSettings model** (add to `Core/Models/AppSettings.cs`):
    ```csharp
    namespace SharepointToolbox.Core.Models;
    public class AppSettings
    {
        public string DataFolder { get; set; } = string.Empty;
        public string Lang { get; set; } = "en";
    }
    ```
    Note: STJ with `PropertyNamingPolicy.CamelCase` will serialize `DataFolder`→`dataFolder`, `Lang`→`lang`.

    **SettingsRepository.cs** — same write-then-replace pattern as ProfileRepository:
    - `Task<AppSettings> LoadAsync()` — returns `new AppSettings()` if file missing; throws `InvalidDataException` on corrupt JSON
    - `Task SaveAsync(AppSettings settings)` — write-then-replace with `SemaphoreSlim(1)` and camelCase serialization

    **SettingsService.cs**:
    - Constructor takes `SettingsRepository`
    - `Task<AppSettings> GetSettingsAsync()`
    - `Task SetLanguageAsync(string cultureCode)` — validates "en" or "fr"; throws `ArgumentException` otherwise
    - `Task SetDataFolderAsync(string path)` — saves path (empty string allowed — means default)

    **SettingsServiceTests.cs** — Replace stub with real tests using temp file.
    All tests in `[Trait("Category", "Unit")]`.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~SettingsServiceTests" 2>&1 | tail -10</automated>
  </verify>
  <done>All SettingsServiceTests pass. AppSettings serializes to dataFolder/lang (camelCase). Default settings returned when file is absent.</done>
</task>

</tasks>

<verification>
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "Category=Unit"` — all pass
- JSON output from ProfileRepository contains `"profiles"` root key with `"name"`, `"tenantUrl"`, `"clientId"` field names
- JSON output from SettingsRepository contains `"dataFolder"` and `"lang"` field names
- Both repositories use `SemaphoreSlim(1)` write lock
- Both repositories use write-then-replace (`.tmp` file then `File.Move`)
</verification>

<success_criteria>
Unit tests green for ProfileService and SettingsService. JSON schema compatibility verified by test assertions on serialized output. Write-then-replace pattern protects against crash-corruption.
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
</output>
