using Campaign_Tracker.Server.ExtensionData; using Campaign_Tracker.Server.LegacyData; using Campaign_Tracker.Server.LegacyData.Models; using Campaign_Tracker.Server.Municipalities; namespace Campaign_Tracker.Server.Tests; public sealed class MunicipalityProfileRepositoryTests { private static readonly DateTimeOffset FixedNow = new(2026, 5, 6, 12, 0, 0, TimeSpan.Zero); private static InMemoryMunicipalityProfileRepository BuildSut( ILegacyDataAccess? data = null) { data ??= new InMemoryLegacyDataAccess( jurisdictions: [ new("FAIR01", "Fairview Borough", "100 Main St", "Fairview, PA 16415", null, null), new("LAKE02", "Lake Township", "200 Lake Rd", "Lake City, PA 16423", null, null), ]); return new InMemoryMunicipalityProfileRepository( new LegacyLinkValidator(data), data, new FakeTimeProvider(FixedNow)); } // ── AC #1: profile saved with required JCode link ──────────────────────── [Fact] public async Task CreateAsync_ValidJCode_SavesProfileWithLegacyLink_AC1() { var sut = BuildSut(); var result = await sut.CreateAsync("FAIR01", "Fairview Profile", "user@test.com"); Assert.True(result.Saved); Assert.NotNull(result.Profile); Assert.Equal("FAIR01", result.Profile.JCode); Assert.Equal("MunicipalityProfile", result.Profile.RecordType); Assert.Equal(LegacyLinkType.JurisdictionJCode, result.Profile.LegacyLink.Type); Assert.Equal("FAIR01", result.Profile.LegacyLink.Value); } [Fact] public async Task CreateAsync_ProfileIdIsGeneratedGuid_AC1() { var sut = BuildSut(); var result = await sut.CreateAsync("FAIR01", null, "user@test.com"); Assert.NotNull(result.Profile); Assert.True(Guid.TryParse(result.Profile.ProfileId, out _), "ProfileId should be a valid GUID."); } // ── AC #2: combined view resolves legacy jurisdiction data ──────────────── [Fact] public async Task GetAllAsync_ReturnsLegacyJurisdictionFieldsAlongsideExtensionData_AC2() { var sut = BuildSut(); await sut.CreateAsync("FAIR01", "Fairview Display", "user@test.com"); var views = await sut.GetAllAsync(); Assert.Single(views); var view = views[0]; Assert.Equal("FAIR01", view.Profile.JCode); Assert.Equal("Fairview Borough", view.LegacyName); Assert.Equal("100 Main St", view.LegacyMailingAddress); Assert.Equal("Fairview, PA 16415", view.LegacyCityStateZip); } [Fact] public async Task GetByIdAsync_ReturnsLegacyAndExtensionDataCombined_AC2() { var sut = BuildSut(); var created = await sut.CreateAsync("LAKE02", null, "user@test.com"); var view = await sut.GetByIdAsync(created.Profile!.ProfileId); Assert.NotNull(view); Assert.Equal("LAKE02", view.Profile.JCode); Assert.Equal("Lake Township", view.LegacyName); } [Fact] public async Task GetByIdAsync_UnknownId_ReturnsNull_AC2() { var sut = BuildSut(); var view = await sut.GetByIdAsync("does-not-exist"); Assert.Null(view); } // ── AC #3: update records actor and timestamp ───────────────────────────── [Fact] public async Task UpdateAsync_ChangesDisplayNameAndCapturesActor_AC3() { var sut = BuildSut(); var created = await sut.CreateAsync("FAIR01", "Old Name", "creator@test.com"); var updated = await sut.UpdateAsync(created.Profile!.ProfileId, "New Name", "updater@test.com"); Assert.True(updated.Saved); Assert.Equal("New Name", updated.Profile!.DisplayName); Assert.Equal("updater@test.com", updated.Profile.UpdatedBy); Assert.Equal(FixedNow, updated.Profile.UpdatedAt); } [Fact] public async Task UpdateAsync_UnknownId_ReturnsFailure_AC3() { var sut = BuildSut(); var result = await sut.UpdateAsync("ghost-id", "name", "actor"); Assert.False(result.Saved); Assert.Contains("ghost-id", result.Error); } // ── AC #4: invalid JCode rejected before save ───────────────────────────── [Fact] public async Task CreateAsync_InvalidJCode_ReturnsFailureWithDescription_AC4() { var sut = BuildSut(new InMemoryLegacyDataAccess(jurisdictions: [])); var result = await sut.CreateAsync("UNKNOWN", null, "user@test.com"); Assert.False(result.Saved); Assert.NotNull(result.Error); Assert.Contains("UNKNOWN", result.Error); } [Fact] public async Task CreateAsync_BlankJCode_ReturnsFailureWithDescription_AC4() { var sut = BuildSut(); var result = await sut.CreateAsync("", null, "user@test.com"); Assert.False(result.Saved); Assert.NotNull(result.Error); } [Fact] public async Task CreateAsync_DuplicateJCode_ReturnsFailure_AC4() { var sut = BuildSut(); await sut.CreateAsync("FAIR01", null, "user@test.com"); var second = await sut.CreateAsync("FAIR01", "Another", "user@test.com"); Assert.False(second.Saved); Assert.Contains("FAIR01", second.Error); } // ── ILegacyLinkedRecordProvider participates in integrity check ────────── [Fact] public async Task GetAllAsync_AsLinkedRecordProvider_ReturnsProfilesForIntegrityCheck_AC1() { var sut = BuildSut(); await sut.CreateAsync("FAIR01", null, "user@test.com"); await sut.CreateAsync("LAKE02", null, "user@test.com"); var provider = (ILegacyLinkedRecordProvider)sut; var records = await provider.GetAllAsync(); Assert.Equal(2, records.Count); Assert.All(records, r => { Assert.Equal("MunicipalityProfile", r.RecordType); Assert.Equal(LegacyLinkType.JurisdictionJCode, r.LegacyLink.Type); }); } // ── Helpers ─────────────────────────────────────────────────────────────── private sealed class FakeTimeProvider(DateTimeOffset utcNow) : TimeProvider { public override DateTimeOffset GetUtcNow() => utcNow; } }