using Dapper;
using FluentAssertions;
using Microsoft.Data.SqlClient;
using Respawn;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Infrastructure.Persistence;

namespace SIGCM2.Application.Tests.Infrastructure;

/// <summary>
/// Integration tests for TipoDeIvaRepository against SIGCM2_Test.
/// NOTE: TipoDeIva is in Respawn TablesToIgnore (seed data must survive resets).
/// Tests insert their own rows and identify them by the returned Id.
/// VigenciaDesde dates are chosen to be unique per test class to avoid UQ violations.
/// </summary>
[Collection("Database")]
public class TipoDeIvaRepositoryTests : IAsyncLifetime
{
    private const string ConnectionString = TestConnectionStrings.AppTestDb;

    private SqlConnection _connection = null!;
    private ITipoDeIvaRepository _repo = null!;

    // TipoDeIva rows are never Respawned (seed data). We need a globally unique VigenciaDesde
    // per (Codigo, VigenciaDesde) pair across all test runs.
    // Strategy: derive a large pseudo-random day offset from a Guid generated once per
    // process + an Interlocked counter for intra-run uniqueness.
    // This makes the probability of collision between runs astronomically small.
    private static readonly int _runBase;

    static TipoDeIvaRepositoryTests()
    {
        // Take the first 4 bytes of a fresh Guid as a pseudo-random int in [0, 500_000)
        var bytes = Guid.NewGuid().ToByteArray();
        _runBase = (int)(BitConverter.ToUInt32(bytes, 0) % 500_000u);
    }

    private static int _testCounter = 0;

    private static DateOnly NextUniqueDate()
    {
        var counter = Interlocked.Increment(ref _testCounter);
        return new DateOnly(2090, 1, 1).AddDays(_runBase + counter);
    }

    public async Task InitializeAsync()
    {
        _connection = new SqlConnection(ConnectionString);
        await _connection.OpenAsync();

        var factory = new SqlConnectionFactory(ConnectionString);
        _repo = new TipoDeIvaRepository(factory);
    }

    public async Task DisposeAsync()
    {
        await _connection.CloseAsync();
        await _connection.DisposeAsync();
    }

    // ── T400.1 / T400.4: InsertAsync + GetByIdAsync ───────────────────────────

    [Fact]
    public async Task InsertAsync_ReturnsPositiveId_AndRowIsVisibleInGetById()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation(
            codigo: "IVA_21",
            descripcion: "Test IVA 21%",
            porcentaje: 21m,
            aplicaIVA: true,
            vigenciaDesde: vigencia);

        var id = await _repo.InsertAsync(entity);

        id.Should().BeGreaterThan(0);

        var fetched = await _repo.GetByIdAsync(id);
        fetched.Should().NotBeNull();
        fetched!.Id.Should().Be(id);
        fetched.Codigo.Should().Be("IVA_21");
        fetched.Descripcion.Should().Be("Test IVA 21%");
        fetched.Porcentaje.Should().Be(21m);
        fetched.AplicaIVA.Should().BeTrue();
        fetched.Activo.Should().BeTrue();
        fetched.VigenciaDesde.Should().Be(vigencia);
        fetched.VigenciaHasta.Should().BeNull();
        fetched.PredecesorId.Should().BeNull();
    }

    [Fact]
    public async Task GetByIdAsync_NonExistentId_ReturnsNull()
    {
        var result = await _repo.GetByIdAsync(999_999_999);

        result.Should().BeNull();
    }

    // ── T400.7: InsertAsync con Codigo duplicado en misma VigenciaDesde ───────

    [Fact]
    public async Task InsertAsync_DuplicateCodigo_SameVigenciaDesde_ThrowsDuplicateCodigoException()
    {
        var vigencia = NextUniqueDate();
        var first = TipoDeIva.ForCreation("IVA_21", "Primero", 21m, true, vigencia);
        await _repo.InsertAsync(first);

        var second = TipoDeIva.ForCreation("IVA_21", "Segundo", 21m, true, vigencia);
        var act = async () => await _repo.InsertAsync(second);

        await act.Should().ThrowAsync<DuplicateCodigoException>();
    }

    // ── UpdateCosmeticoAsync ──────────────────────────────────────────────────

    [Fact]
    public async Task UpdateCosmeticoAsync_ChangesOnlyCosmeticFields_NotPorcentajeOrVigencias()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation("IVA_21", "Original Desc", 21m, true, vigencia);
        var id = await _repo.InsertAsync(entity);

        var result = await _repo.UpdateCosmeticoAsync(id, "IVA_21", "Updated Desc", false, true);

        result.Should().BeTrue();

        var fetched = await _repo.GetByIdAsync(id);
        fetched!.Descripcion.Should().Be("Updated Desc");
        fetched.AplicaIVA.Should().BeFalse();
        // Immutable fields must NOT change
        fetched.Porcentaje.Should().Be(21m);
        fetched.VigenciaDesde.Should().Be(vigencia);
        fetched.VigenciaHasta.Should().BeNull();
        fetched.PredecesorId.Should().BeNull();
    }

    // ── UpdateCierreVigenciaAsync ─────────────────────────────────────────────

    [Fact]
    public async Task UpdateCierreVigenciaAsync_SetsVigenciaHasta_WhenRowIsOpen()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation("IVA_21", "Cierre test", 21m, true, vigencia);
        var id = await _repo.InsertAsync(entity);

        var closeDate = vigencia.AddMonths(6);
        var result = await _repo.UpdateCierreVigenciaAsync(id, closeDate);

        result.Should().BeTrue();

        var fetched = await _repo.GetByIdAsync(id);
        fetched!.VigenciaHasta.Should().Be(closeDate);
    }

    // ── T400.6: Race double-close guard ──────────────────────────────────────

    [Fact]
    public async Task UpdateCierreVigenciaAsync_DoubleClose_ReturnsFalseOnSecondCall()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation("IVA_21", "Double close", 21m, true, vigencia);
        var id = await _repo.InsertAsync(entity);

        var closeDate = vigencia.AddMonths(6);
        var first = await _repo.UpdateCierreVigenciaAsync(id, closeDate);
        var second = await _repo.UpdateCierreVigenciaAsync(id, closeDate.AddMonths(1));

        first.Should().BeTrue();
        second.Should().BeFalse("the guard prevents double-close: WHERE VigenciaHasta IS NULL");
    }

    // ── SetActivoAsync ────────────────────────────────────────────────────────

    [Fact]
    public async Task SetActivoAsync_FalseAndBack_TogglesActivoCorrectly()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation("IVA_21", "Toggle test", 21m, true, vigencia);
        var id = await _repo.InsertAsync(entity);

        await _repo.SetActivoAsync(id, false);
        var inactive = await _repo.GetByIdAsync(id);
        inactive!.Activo.Should().BeFalse();

        await _repo.SetActivoAsync(id, true);
        var active = await _repo.GetByIdAsync(id);
        active!.Activo.Should().BeTrue();
    }

    // ── ListAsync con paginación + filtros ────────────────────────────────────

    [Fact]
    public async Task ListAsync_WithActivoFilter_ReturnsOnlyMatchingRows()
    {
        // Insert one active and one inactive in their own date range
        var d1 = NextUniqueDate();
        var d2 = NextUniqueDate();

        var activeId = await _repo.InsertAsync(
            TipoDeIva.ForCreation("IVA_21", "Active row", 21m, true, d1));
        var inactiveId = await _repo.InsertAsync(
            TipoDeIva.ForCreation("IVA_21", "Inactive row", 21m, true, d2));
        await _repo.SetActivoAsync(inactiveId, false);

        var result = await _repo.ListAsync(new TiposDeIvaQuery(
            Page: 1,
            PageSize: 100,
            Activo: true,
            Codigo: null));

        result.Items.Should().Contain(x => x.Id == activeId);
        result.Items.Should().NotContain(x => x.Id == inactiveId);
    }

    [Fact]
    public async Task ListAsync_ReturnsCorrectTotalCount()
    {
        // Insert a row with a unique Codigo prefix we can filter on
        var vigencia = NextUniqueDate();
        await _repo.InsertAsync(
            TipoDeIva.ForCreation("EXENTO", "Total count test", 0m, false, vigencia));

        var result = await _repo.ListAsync(new TiposDeIvaQuery(
            Page: 1,
            PageSize: 10,
            Activo: null,
            Codigo: "EXENTO"));

        result.Total.Should().BeGreaterThan(0);
        result.Items.Should().AllSatisfy(x => x.Codigo.Should().StartWith("EXENTO"));
    }

    // ── T400.5: Cadena de 3 versiones ────────────────────────────────────────

    [Fact]
    public async Task VersionChain_ThreeVersions_PredecesorIdChainIsCorrect()
    {
        // v1: IVA_21 at 21%
        var v1Date = NextUniqueDate();
        var v1 = TipoDeIva.ForCreation("IVA_21", "Version 1", 21m, true, v1Date);
        var v1Id = await _repo.InsertAsync(v1);

        // Close v1 vigencia
        var v2Date = v1Date.AddMonths(1);
        await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));

        // v2: 23%
        var v2 = TipoDeIva.ForCreation("IVA_21", "Version 2", 23m, true, v2Date, null, v1Id);
        var v2Id = await _repo.InsertAsync(v2);

        // Close v2 vigencia
        var v3Date = v2Date.AddMonths(1);
        await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));

        // v3: 25%
        var v3 = TipoDeIva.ForCreation("IVA_21", "Version 3", 25m, true, v3Date, null, v2Id);
        var v3Id = await _repo.InsertAsync(v3);

        // Verify chain
        var fv2 = await _repo.GetByIdAsync(v2Id);
        var fv3 = await _repo.GetByIdAsync(v3Id);

        fv2!.PredecesorId.Should().Be(v1Id);
        fv3!.PredecesorId.Should().Be(v2Id);
    }

    // ── T400.8: GetHistorialAsync ─────────────────────────────────────────────

    [Fact]
    public async Task GetHistorialAsync_ReturnsChainFromRootToId_OrderedByVigenciaDesdeAsc()
    {
        var v1Date = NextUniqueDate();
        var v1 = TipoDeIva.ForCreation("IVA_21", "Hist v1", 21m, true, v1Date);
        var v1Id = await _repo.InsertAsync(v1);

        var v2Date = v1Date.AddMonths(1);
        await _repo.UpdateCierreVigenciaAsync(v1Id, v2Date.AddDays(-1));
        var v2 = TipoDeIva.ForCreation("IVA_21", "Hist v2", 23m, true, v2Date, null, v1Id);
        var v2Id = await _repo.InsertAsync(v2);

        var v3Date = v2Date.AddMonths(1);
        await _repo.UpdateCierreVigenciaAsync(v2Id, v3Date.AddDays(-1));
        var v3 = TipoDeIva.ForCreation("IVA_21", "Hist v3", 25m, true, v3Date, null, v2Id);
        var v3Id = await _repo.InsertAsync(v3);

        // Get historial from v3Id — should return chain v1, v2, v3
        var historial = await _repo.GetHistorialAsync(v3Id);

        historial.Should().HaveCount(3);
        historial[0].Id.Should().Be(v1Id, "root is first");
        historial[1].Id.Should().Be(v2Id);
        historial[2].Id.Should().Be(v3Id, "requested Id is last");
        historial[0].VigenciaDesde.Should().BeBefore(historial[1].VigenciaDesde);
        historial[1].VigenciaDesde.Should().BeBefore(historial[2].VigenciaDesde);
    }

    [Fact]
    public async Task GetHistorialAsync_SingleVersion_ReturnsListWithOneItem()
    {
        var vigencia = NextUniqueDate();
        var entity = TipoDeIva.ForCreation("IVA_21", "Solo", 21m, true, vigencia);
        var id = await _repo.InsertAsync(entity);

        var historial = await _repo.GetHistorialAsync(id);

        historial.Should().HaveCount(1);
        historial[0].Id.Should().Be(id);
    }
}
