using Dapper; using SIGCM2.Domain.Entities; using SIGCM2.Infrastructure.Persistence; using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Rubros; /// /// Integration tests for RubroRepository against SIGCM2_Test_App. /// Uses shared SqlTestFixture via [Collection("Database")] — fixture maneja Respawn + seeds. /// Temporal: after UpdateAsync, dbo.Rubro_History MUST have ≥1 row for that Id. /// [Collection("Database")] public class RubroRepositoryTests : IAsyncLifetime { private readonly SqlTestFixture _db; private RubroRepository _repository = null!; private TimeProvider _timeProvider = null!; public RubroRepositoryTests(SqlTestFixture db) { _db = db; } public async Task InitializeAsync() { await _db.ResetAndSeedAsync(); var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb); _repository = new RubroRepository(factory); _timeProvider = TimeProvider.System; } public Task DisposeAsync() => Task.CompletedTask; // ── AddAsync + GetByIdAsync roundtrip ───────────────────────────────────── [Fact] public async Task AddAsync_AndGetById_ReturnsAllFields() { var rubro = Rubro.ForCreation("Automotores", parentId: null, orden: 0, tarifarioBaseId: null, _timeProvider); var id = await _repository.AddAsync(rubro); var result = await _repository.GetByIdAsync(id); Assert.NotNull(result); Assert.Equal(id, result!.Id); Assert.Equal("Automotores", result.Nombre); Assert.Null(result.ParentId); Assert.Equal(0, result.Orden); Assert.True(result.Activo); Assert.Null(result.TarifarioBaseId); Assert.True(result.FechaCreacion > DateTime.MinValue); Assert.Null(result.FechaModificacion); } [Fact] public async Task AddAsync_WithTarifarioBaseId_PersistsValue() { var rubro = Rubro.ForCreation("Tecnología", parentId: null, orden: 0, tarifarioBaseId: 42, _timeProvider); var id = await _repository.AddAsync(rubro); var result = await _repository.GetByIdAsync(id); Assert.NotNull(result); Assert.Equal(42, result!.TarifarioBaseId); } [Fact] public async Task GetByIdAsync_NonExistent_ReturnsNull() { var result = await _repository.GetByIdAsync(999999); Assert.Null(result); } // ── GetAllAsync ──────────────────────────────────────────────────────────── [Fact] public async Task GetAllAsync_IncluirInactivosFalse_OmitsInactive() { var activo = Rubro.ForCreation("Activo", null, 0, null, _timeProvider); var inactivo = Rubro.ForCreation("Inactivo", null, 1, null, _timeProvider); await _repository.AddAsync(activo); var inactivoId = await _repository.AddAsync(inactivo); // Deactivate the second one via UpdateAsync var inactivoEntity = await _repository.GetByIdAsync(inactivoId); await _repository.UpdateAsync(inactivoEntity!.WithActivo(false, _timeProvider)); var all = await _repository.GetAllAsync(incluirInactivos: false); Assert.Contains(all, r => r.Nombre == "Activo"); Assert.DoesNotContain(all, r => r.Nombre == "Inactivo"); } [Fact] public async Task GetAllAsync_IncluirInactivosTrue_ReturnsAll() { var activo = Rubro.ForCreation("ActivoAll", null, 0, null, _timeProvider); var inactivo = Rubro.ForCreation("InactivoAll", null, 1, null, _timeProvider); await _repository.AddAsync(activo); var inactivoId = await _repository.AddAsync(inactivo); var inactivoEntity = await _repository.GetByIdAsync(inactivoId); await _repository.UpdateAsync(inactivoEntity!.WithActivo(false, _timeProvider)); var all = await _repository.GetAllAsync(incluirInactivos: true); Assert.Contains(all, r => r.Nombre == "ActivoAll"); Assert.Contains(all, r => r.Nombre == "InactivoAll"); } // ── GetDescendantsAsync ──────────────────────────────────────────────────── [Fact] public async Task GetDescendantsAsync_3LevelsDeep_ReturnsAllDescendants() { // Level 0: root var root = Rubro.ForCreation("Root", null, 0, null, _timeProvider); var rootId = await _repository.AddAsync(root); // Level 1: child of root var child = Rubro.ForCreation("Child", rootId, 0, null, _timeProvider); var childId = await _repository.AddAsync(child); // Level 2: grandchild of root var grandchild = Rubro.ForCreation("Grandchild", childId, 0, null, _timeProvider); var grandchildId = await _repository.AddAsync(grandchild); var descendants = await _repository.GetDescendantsAsync(rootId); Assert.Equal(2, descendants.Count); Assert.Contains(descendants, d => d.Id == childId); Assert.Contains(descendants, d => d.Id == grandchildId); } [Fact] public async Task GetDescendantsAsync_Leaf_ReturnsEmpty() { var leaf = Rubro.ForCreation("Leaf", null, 0, null, _timeProvider); var leafId = await _repository.AddAsync(leaf); var descendants = await _repository.GetDescendantsAsync(leafId); Assert.Empty(descendants); } // ── CountActiveChildrenAsync ─────────────────────────────────────────────── [Fact] public async Task CountActiveChildrenAsync_NoChildren_ReturnsZero() { var parent = Rubro.ForCreation("ParentNoHijos", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); var count = await _repository.CountActiveChildrenAsync(parentId); Assert.Equal(0, count); } [Fact] public async Task CountActiveChildrenAsync_TwoActive_ReturnsTwo() { var parent = Rubro.ForCreation("ParentDosHijos", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); await _repository.AddAsync(Rubro.ForCreation("Hijo1", parentId, 0, null, _timeProvider)); await _repository.AddAsync(Rubro.ForCreation("Hijo2", parentId, 1, null, _timeProvider)); var count = await _repository.CountActiveChildrenAsync(parentId); Assert.Equal(2, count); } [Fact] public async Task CountActiveChildrenAsync_ActiveAndInactive_CountsOnlyActive() { var parent = Rubro.ForCreation("ParentMixed", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); await _repository.AddAsync(Rubro.ForCreation("HijoActivo", parentId, 0, null, _timeProvider)); var inactivoId = await _repository.AddAsync(Rubro.ForCreation("HijoInactivo", parentId, 1, null, _timeProvider)); var inactivo = await _repository.GetByIdAsync(inactivoId); await _repository.UpdateAsync(inactivo!.WithActivo(false, _timeProvider)); var count = await _repository.CountActiveChildrenAsync(parentId); Assert.Equal(1, count); } // ── GetMaxOrdenAsync ─────────────────────────────────────────────────────── [Fact] public async Task GetMaxOrdenAsync_Empty_ReturnsZero() { // No siblings → first slot is 0 var orden = await _repository.GetMaxOrdenAsync(parentId: null); Assert.Equal(0, orden); } [Fact] public async Task GetMaxOrdenAsync_ThreeSiblings_ReturnsThree() { // Orden = [0, 1, 2] → next slot = 3 (MAX+1) var parent = Rubro.ForCreation("ParentOrden", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); await _repository.AddAsync(Rubro.ForCreation("S1", parentId, 0, null, _timeProvider)); await _repository.AddAsync(Rubro.ForCreation("S2", parentId, 1, null, _timeProvider)); await _repository.AddAsync(Rubro.ForCreation("S3", parentId, 2, null, _timeProvider)); var orden = await _repository.GetMaxOrdenAsync(parentId); Assert.Equal(3, orden); } [Fact] public async Task GetMaxOrdenAsync_ParentIdNull_WorksForRoots() { // Insert one root with Orden=0 await _repository.AddAsync(Rubro.ForCreation("RootOrden1", null, 0, null, _timeProvider)); var orden = await _repository.GetMaxOrdenAsync(parentId: null); Assert.Equal(1, orden); // MAX(0) + 1 = 1 } // ── ExistsByNombreUnderParentAsync ───────────────────────────────────────── [Fact] public async Task ExistsByNombreUnderParentAsync_Exists_ReturnsTrue() { var parent = Rubro.ForCreation("ParentExists", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); await _repository.AddAsync(Rubro.ForCreation("Autos", parentId, 0, null, _timeProvider)); var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "Autos", excludeId: null); Assert.True(exists); } [Fact] public async Task ExistsByNombreUnderParentAsync_SameNameDifferentParent_ReturnsFalse() { var parent1 = Rubro.ForCreation("Parent1", null, 0, null, _timeProvider); var parent1Id = await _repository.AddAsync(parent1); var parent2 = Rubro.ForCreation("Parent2", null, 1, null, _timeProvider); var parent2Id = await _repository.AddAsync(parent2); // Add "Autos" under parent1 await _repository.AddAsync(Rubro.ForCreation("Autos", parent1Id, 0, null, _timeProvider)); // Check under parent2 — should not exist var exists = await _repository.ExistsByNombreUnderParentAsync(parent2Id, "Autos", excludeId: null); Assert.False(exists); } [Fact] public async Task ExistsByNombreUnderParentAsync_ExcludeId_WhenProvidedSkipsSelf() { var parent = Rubro.ForCreation("ParentExclude", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); var id = await _repository.AddAsync(Rubro.ForCreation("AutosExclude", parentId, 0, null, _timeProvider)); // Excluding self → should return false (no other rubro with same name) var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "AutosExclude", excludeId: id); Assert.False(exists); } [Fact] public async Task ExistsByNombreUnderParentAsync_CaseInsensitive_InsensibleAMayusculas() { var parent = Rubro.ForCreation("ParentCI", null, 0, null, _timeProvider); var parentId = await _repository.AddAsync(parent); await _repository.AddAsync(Rubro.ForCreation("autos", parentId, 0, null, _timeProvider)); // "AUTOS" should match "autos" (case-insensitive) var exists = await _repository.ExistsByNombreUnderParentAsync(parentId, "AUTOS", excludeId: null); Assert.True(exists); } [Fact] public async Task ExistsByNombreUnderParentAsync_ForRoot_ParentIdNull_WorksWithApplicationDefense() { // The DB filtered index only covers non-root rubros (WHERE ParentId IS NOT NULL AND Activo = 1). // Application must check roots via full scan (no unique index guarantee at DB level). await _repository.AddAsync(Rubro.ForCreation("RootCI", null, 0, null, _timeProvider)); var exists = await _repository.ExistsByNombreUnderParentAsync(null, "RootCI", excludeId: null); Assert.True(exists); } // ── GetDepthAsync ────────────────────────────────────────────────────────── [Fact] public async Task GetDepthAsync_RootParent_ReturnsZero() { // parentId = null means we're creating a root → depth = 0 var depth = await _repository.GetDepthAsync(parentId: null); Assert.Equal(0, depth); } [Fact] public async Task GetDepthAsync_3LevelsDeep_ReturnsThree() { // root (depth 0) → child (depth 1) → grandchild (depth 2) → great-grandchild (depth 3) var rootId = await _repository.AddAsync(Rubro.ForCreation("RootDepth", null, 0, null, _timeProvider)); var childId = await _repository.AddAsync(Rubro.ForCreation("ChildDepth", rootId, 0, null, _timeProvider)); var grandchildId = await _repository.AddAsync(Rubro.ForCreation("GrandchildDepth", childId, 0, null, _timeProvider)); // Depth of the grandchild's own id as parentId = 3 levels deep (root=1, child=2, grandchild=3) var depth = await _repository.GetDepthAsync(grandchildId); Assert.Equal(3, depth); } // ── UpdateAsync + Temporal ──────────────────────────────────────────────── [Fact] public async Task UpdateAsync_ModificaCamposYProduceHistoryRow() { var id = await _repository.AddAsync(Rubro.ForCreation("Original", null, 0, null, _timeProvider)); var original = await _repository.GetByIdAsync(id); var renamed = original!.WithRenamed("Actualizado", _timeProvider); await _repository.UpdateAsync(renamed); var result = await _repository.GetByIdAsync(id); Assert.NotNull(result); Assert.Equal("Actualizado", result!.Nombre); Assert.NotNull(result.FechaModificacion); var historyCount = await _db.Connection.ExecuteScalarAsync( "SELECT COUNT(*) FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id }); Assert.True(historyCount >= 1, $"Expected ≥1 history row for Rubro Id={id}, got {historyCount}"); } [Fact] public async Task SoftDeleteAsync_FlipActivoYActualizaFechaModificacion() { // Deactivate via UpdateAsync (with WithActivo(false)) — repository has no separate SoftDelete var id = await _repository.AddAsync(Rubro.ForCreation("ToDeactivate", null, 0, null, _timeProvider)); var rubro = await _repository.GetByIdAsync(id); var deactivated = rubro!.WithActivo(false, _timeProvider); await _repository.UpdateAsync(deactivated); var result = await _repository.GetByIdAsync(id); Assert.NotNull(result); Assert.False(result!.Activo); Assert.NotNull(result.FechaModificacion); } [Fact] public async Task MoveAsync_CambiaParentIdOrdenYFechaModificacion() { var parent1Id = await _repository.AddAsync(Rubro.ForCreation("MoveParent1", null, 0, null, _timeProvider)); var parent2Id = await _repository.AddAsync(Rubro.ForCreation("MoveParent2", null, 1, null, _timeProvider)); var childId = await _repository.AddAsync(Rubro.ForCreation("MoveChild", parent1Id, 0, null, _timeProvider)); var child = await _repository.GetByIdAsync(childId); var moved = child!.WithMoved(parent2Id, 5, _timeProvider); await _repository.UpdateAsync(moved); var result = await _repository.GetByIdAsync(childId); Assert.NotNull(result); Assert.Equal(parent2Id, result!.ParentId); Assert.Equal(5, result.Orden); Assert.NotNull(result.FechaModificacion); } }