using System.Data.Common; using Dapper; using FrameworkBLL.Engine; using Microsoft.Data.Sqlite; using Xunit; namespace FrameworkTests; /// /// Covers ConflictResolver's three conflict policies (docs/Platform-Architecture-Integration-Reference.md /// §5.1/§9.6) against a real in-memory SQLite database — not mocks. ConflictResolver's own SQL /// (identifier-quoted DELETE/INSERT/SELECT-by-PK/UPDATE) needs no OFFSET/FETCH or other /// SQL-Server/Postgres-specific syntax, and SQLite genuinely accepts bracket-quoted identifiers /// ([Col]) for compatibility — so this is real behavioral proof, not a syntax-compatible fiction. /// Internal-only class, reached via [InternalsVisibleTo("FrameworkTests")] (see /// FrameworkBLL/Properties/AssemblyInfo.cs). /// public class ConflictResolverTests : IAsyncLifetime { private SqliteConnection _conn = null!; public async Task InitializeAsync() { _conn = new SqliteConnection("Data Source=:memory:"); await _conn.OpenAsync(); // ModifiedOn is declared DATETIME (not TEXT) so Microsoft.Data.Sqlite's type-affinity // mapping returns a real CLR DateTime for dynamic/object reads, matching what a genuine // SQL Server/Postgres native datetime column would give ConflictResolver in production. await _conn.ExecuteAsync("CREATE TABLE TestTable (Id INTEGER PRIMARY KEY, Value TEXT, ModifiedOn DATETIME)"); } public async Task DisposeAsync() => await _conn.DisposeAsync(); private static Dictionary Row(int id, string value, DateTime? modifiedOn = null) => new() { ["Id"] = id, ["Value"] = value, ["ModifiedOn"] = modifiedOn }; private async Task SeedAsync(int id, string value, DateTime? modifiedOn = null) => await _conn.ExecuteAsync( "INSERT INTO TestTable (Id, Value, ModifiedOn) VALUES (@Id, @Value, @ModifiedOn)", new { Id = id, Value = value, ModifiedOn = modifiedOn }); private async Task GetValueAsync(int id) => await _conn.QuerySingleOrDefaultAsync( "SELECT Value FROM TestTable WHERE Id = @Id", new { Id = id }); [Fact] public async Task Overwrite_ExistingRow_IsDeletedThenReinsertedWithNewValue() { await SeedAsync(1, "old"); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "new") }, "Id", ConflictPolicy.Overwrite, null, DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(1, written); Assert.Equal("new", await GetValueAsync(1)); } [Fact] public async Task Overwrite_NewRow_IsInserted() { int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "brand-new") }, "Id", ConflictPolicy.Overwrite, null, DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(1, written); Assert.Equal("brand-new", await GetValueAsync(1)); } [Fact] public async Task Skip_ExistingRow_IsLeftUntouched_NewRowIsInserted() { await SeedAsync(1, "old"); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "attempted-overwrite"), Row(2, "brand-new") }, "Id", ConflictPolicy.Skip, null, DatabaseType.SqlServer, DatabaseType.SqlServer, default); // Only row 2 was genuinely new — row 1 already existed and must be untouched. Assert.Equal(1, written); Assert.Equal("old", await GetValueAsync(1)); Assert.Equal("brand-new", await GetValueAsync(2)); } [Fact] public async Task LastWriteWins_SourceNewerThanTarget_Updates() { var older = new DateTime(2026, 1, 1); var newer = new DateTime(2026, 1, 2); await SeedAsync(1, "old", older); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "new", newer) }, "Id", ConflictPolicy.LastWriteWins, "ModifiedOn", DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(1, written); Assert.Equal("new", await GetValueAsync(1)); } [Fact] public async Task LastWriteWins_SourceOlderThanTarget_LeavesTargetUntouched() { var older = new DateTime(2026, 1, 1); var newer = new DateTime(2026, 1, 2); await SeedAsync(1, "authoritative", newer); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "stale-source-value", older) }, "Id", ConflictPolicy.LastWriteWins, "ModifiedOn", DatabaseType.SqlServer, DatabaseType.SqlServer, default); // Source is older than target — must be skipped, not overwritten. Assert.Equal(0, written); Assert.Equal("authoritative", await GetValueAsync(1)); } [Fact] public async Task LastWriteWins_RowAbsentFromTarget_IsInserted() { int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "brand-new", DateTime.UtcNow) }, "Id", ConflictPolicy.LastWriteWins, "ModifiedOn", DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(1, written); Assert.Equal("brand-new", await GetValueAsync(1)); } [Fact] public async Task LastWriteWins_NoModifiedDateColumnConfigured_FallsBackToOverwrite() { await SeedAsync(1, "old"); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", new[] { Row(1, "new") }, "Id", ConflictPolicy.LastWriteWins, modifiedDateColumn: null, DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(1, written); Assert.Equal("new", await GetValueAsync(1)); } [Fact] public async Task ApplyAsync_EmptyChunk_ReturnsZeroWithoutTouchingTarget() { await SeedAsync(1, "untouched"); int written = await ConflictResolver.ApplyAsync( (DbConnection)_conn, "", "TestTable", Array.Empty>(), "Id", ConflictPolicy.Overwrite, null, DatabaseType.SqlServer, DatabaseType.SqlServer, default); Assert.Equal(0, written); Assert.Equal("untouched", await GetValueAsync(1)); } }