using Dapper;
using SIGCM2.Domain.Entities;
using SIGCM2.Infrastructure.Persistence;
using SIGCM2.TestSupport;

namespace SIGCM2.Application.Tests.Rubros;

/// <summary>
/// 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.
/// </summary>
[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<int>(
            "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);
    }

}
