using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using NUnit.Framework; using Strata.ContinuousImprovement.Biz.DbContexts; using Strata.ContinuousImprovement.Biz.FreeForm.Opportunities; using Strata.ContinuousImprovement.Biz.Generics; using Strata.ContinuousImprovement.Biz.QualityVariation.Opportunities; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Strata.ContinuousImprovement.Biz.Test.Unit.DbContexts { [TestFixture, Category("Unit")] public class JazzDbContextTests { private JazzDbContext _context; private DbContextOptions _options; [OneTimeSetUp] public void OneTimeSetUp() { _options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}") .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; } [SetUp] public void SetUp() { _context = new JazzDbContext(_options); } [TearDown] public void TearDown() { _context?.Dispose(); } [Test] public void ConfigureConventions_ShouldSetDecimalPrecision_ForAllDecimalProperties() { // Act var model = _context.Model; // Assert - Get all decimal properties from all entities var decimalProperties = model.GetEntityTypes() .SelectMany(e => e.GetProperties()) .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)) .ToList(); // Verify that we have decimal properties to test decimalProperties.Should().NotBeEmpty("There should be decimal properties in the model to test"); // Check that all decimal properties have the expected precision and scale var foregroundColor = Console.ForegroundColor; foreach (var property in decimalProperties) { var className = ((IEntityType)property.DeclaringType).ClrType.Name; var propertyName = property.Name; var precision = property.GetPrecision(); var scale = property.GetScale(); if (precision == null || scale == null) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"Property {className}.{propertyName} does not have precision/scale configured."); } else if (className == "QVIOpportunity" && propertyName == "QVIRate") { precision.Should().Be(19, $"Property {className}.{propertyName} should have precision 19"); scale.Should().Be(4, $"Property {className}.{propertyName} should have scale 4"); } else if (propertyName.StartsWith("Month")) { precision.Should().Be(15, $"Property {className}.{propertyName} should have precision 15"); scale.Should().Be(4, $"Property {className}.{propertyName} should have scale 4"); } else if (propertyName.StartsWith("QVI")) { precision.Should().Be(29, $"Property {className}.{propertyName} should have precision 29"); scale.Should().Be(11, $"Property {className}.{propertyName} should have scale 11"); } } Console.ForegroundColor = foregroundColor; } [Test] public void ConfigureConventions_ShouldNotAffectNonDecimalProperties() { // Act var model = _context.Model; // Assert - Check that non-decimal properties are not affected var nonDecimalProperties = model.GetEntityTypes() .SelectMany(e => e.GetProperties()) .Where(p => p.ClrType != typeof(decimal) && p.ClrType != typeof(decimal?)) .ToList(); nonDecimalProperties.Should().NotBeEmpty("There should be non-decimal properties to verify they're not affected"); foreach (var property in nonDecimalProperties) { var precision = property.GetPrecision(); var scale = property.GetScale(); if (property.ClrType == typeof(string)) { precision.Should().BeNull($"String property {((IEntityType)property.DeclaringType).ClrType.Name}.{property.Name} should not have precision"); scale.Should().BeNull($"String property {((IEntityType)property.DeclaringType).ClrType.Name}.{property.Name} should not have scale"); } } } [Test] public async Task DatabaseCreation_ShouldSucceed_WithDecimalPrecisionConfiguration() { // Act & Assert var canConnect = await _context.Database.CanConnectAsync(); canConnect.Should().BeTrue("Database should be accessible with decimal precision configuration"); await _context.Database.EnsureCreatedAsync(); var created = await _context.Database.CanConnectAsync(); created.Should().BeTrue("Database should be created successfully with decimal precision configuration"); } [Test] public void ModelValidation_ShouldPass_WithDecimalPrecisionConfiguration() { // Arrange & Act Action validateModel = () => { // Accessing the model is enough to trigger validation in EF Core. // No explicit FinalizeModel method exists on IModel. var _ = _context.Model.GetEntityTypes().ToList(); }; // Assert validateModel.Should().NotThrow("Model should validate successfully with decimal precision configuration"); } public static IEnumerable OpportunityTestCases() { yield return new TestCaseData(new QVIOpportunity { OpportunityGuid = Guid.NewGuid(), ConfigurationGuid = Guid.NewGuid(), IdentifiedSavings = (double)98765.432102m, QVIRate = (decimal)98765.432102d, OpportunityKey = "TEST-004" }, (double)98765.4321m, (decimal)98765.4321d); } private void AddOpportunityToContext(Opportunity opportunity) { switch (opportunity) { case QVIOpportunity qviOpportunity: _context.QualityVariationItemOpportunities.Add(qviOpportunity); break; default: throw new ArgumentException($"Unsupported opportunity type: {opportunity.GetType().Name}"); } } private async Task GetSavedOpportunityAsync(Opportunity opportunity) { return opportunity switch { QVIOpportunity qviOpportunity => await _context.QualityVariationItemOpportunities .FirstOrDefaultAsync(o => o.OpportunityGuid == qviOpportunity.OpportunityGuid), _ => throw new ArgumentException($"Unsupported opportunity type: {opportunity.GetType().Name}") }; } [Test, TestCaseSource(nameof(OpportunityTestCases))] public async Task DecimalProperty_ShouldHandlePrecisionCorrectly_InDatabase(Opportunity testOpportunity, double expectedIdentifiedSavings, decimal? expectedQVIRate = null) { // This test verifies that the precision works in practice with the InMemory provider // Note: InMemory provider doesn't enforce precision/scale, but this tests the configuration doesn't break anything // Arrange await _context.Database.EnsureCreatedAsync(); // Act if (testOpportunity == null) { throw new ArgumentNullException(nameof(testOpportunity), "Test opportunity cannot be null"); } AddOpportunityToContext(testOpportunity); // Assert Func saveAction = async () => await _context.SaveChangesAsync(); await saveAction.Should().NotThrowAsync("Saving with configured decimal precision should work"); var savedOpportunity = await GetSavedOpportunityAsync(testOpportunity); savedOpportunity.Should().NotBeNull(); const double ToleranceForDecimalComparison = 19d; if (savedOpportunity is FFOpportunityDataModel ffOppTest) { ffOppTest.IdentifiedSavings.Should().BeApproximately(expectedIdentifiedSavings, ToleranceForDecimalComparison); } else { ((Opportunity)savedOpportunity).IdentifiedSavings.Should().BeApproximately(expectedIdentifiedSavings, ToleranceForDecimalComparison); if (savedOpportunity is QVIOpportunity qviOppTest && expectedQVIRate.HasValue) { qviOppTest.QVIRate.Should().BeApproximately(expectedQVIRate.Value, (decimal)ToleranceForDecimalComparison); } } } [Test] public void BaseMethod_ShouldBeCalled_InConfigureConventions() { // This test ensures that base.ConfigureConventions() is still called // We can't directly test this, but we can verify that EF Core's base conventions are applied var model = _context.Model; // Verify that base EF Core conventions are still working var stringProperties = model.GetEntityTypes() .SelectMany(e => e.GetProperties()) .Where(p => p.ClrType == typeof(string)) .ToList(); stringProperties.Should().NotBeEmpty("Should have string properties to verify base conventions work"); // EF Core base conventions should still work for string properties foreach (var property in stringProperties.Take(5)) // Just check a few { // Base conventions should still be applied (this varies by property, so we just ensure no exceptions) Action getAnnotations = () => { var _ = property.GetAnnotations(); }; getAnnotations.Should().NotThrow($"Base conventions should work for {((IEntityType)property.DeclaringType).ClrType.Name}.{property.Name}"); } } } }