using FluentAssertions;
using SIGCM2.Domain.Entities;

namespace SIGCM2.Application.Tests.Domain;

/// <summary>
/// PRD-003 — Domain unit tests for ProductPrice record.
/// Covers: §REQ-4.1 (IsActive), §REQ-4.2 (CoversDate inclusive bounds),
///          §REQ-4.4 (active row covers any date on or after PriceValidFrom).
/// </summary>
public class ProductPriceTests
{
    private static ProductPrice MakePrice(
        long id = 1,
        int productId = 10,
        decimal price = 1500.00m,
        DateOnly? from = null,
        DateOnly? to = null,
        DateTime? fechaCreacion = null)
        => new(
            Id: id,
            ProductId: productId,
            Price: price,
            PriceValidFrom: from ?? new DateOnly(2026, 4, 1),
            PriceValidTo: to,
            FechaCreacion: fechaCreacion ?? new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));

    // ── §REQ-4.1: IsActive ────────────────────────────────────────────────────

    [Fact]
    public void IsActive_TrueWhenPriceValidToIsNull()
    {
        var price = MakePrice(to: null);

        price.IsActive.Should().BeTrue();
    }

    [Fact]
    public void IsActive_FalseWhenPriceValidToHasValue()
    {
        var price = MakePrice(to: new DateOnly(2026, 4, 30));

        price.IsActive.Should().BeFalse();
    }

    // ── §REQ-4.2: CoversDate — inclusive lower bound ──────────────────────────

    [Fact]
    public void CoversDate_InclusiveLowerBound_ReturnsTrue()
    {
        // Row: 2026-04-01 → 2026-04-30. Query date = PriceValidFrom.
        var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30));

        price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue();
    }

    // ── §REQ-4.2: CoversDate — inclusive upper bound ──────────────────────────

    [Fact]
    public void CoversDate_InclusiveUpperBound_ReturnsTrue()
    {
        // Row: 2026-04-01 → 2026-04-30. Query date = PriceValidTo.
        var price = MakePrice(from: new DateOnly(2026, 4, 1), to: new DateOnly(2026, 4, 30));

        price.CoversDate(new DateOnly(2026, 4, 30)).Should().BeTrue();
    }

    // ── §REQ-4.2: CoversDate — exclusive outside range ────────────────────────

    [Fact]
    public void CoversDate_DateBeforePriceValidFrom_ReturnsFalse()
    {
        var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20));

        price.CoversDate(new DateOnly(2026, 4, 9)).Should().BeFalse();
    }

    [Fact]
    public void CoversDate_DateAfterPriceValidTo_ReturnsFalse()
    {
        var price = MakePrice(from: new DateOnly(2026, 4, 10), to: new DateOnly(2026, 4, 20));

        price.CoversDate(new DateOnly(2026, 4, 21)).Should().BeFalse();
    }

    // ── §REQ-4.4: Active row (PriceValidTo = null) covers any date on or after PriceValidFrom ──

    [Fact]
    public void ActiveRow_CoversAnyDateOnOrAfterPvf()
    {
        // Active row: PriceValidFrom = 2026-04-01, PriceValidTo = null
        var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null);

        // On PriceValidFrom itself
        price.CoversDate(new DateOnly(2026, 4, 1)).Should().BeTrue();
        // Well in the future
        price.CoversDate(new DateOnly(2030, 12, 31)).Should().BeTrue();
    }

    [Fact]
    public void ActiveRow_DoesNotCoverDateBeforePvf()
    {
        var price = MakePrice(from: new DateOnly(2026, 4, 1), to: null);

        price.CoversDate(new DateOnly(2026, 3, 31)).Should().BeFalse();
    }

    // ── Record value equality (records compare by value) ─────────────────────

    [Fact]
    public void Equality_TwoIdenticalRecords_AreEqual()
    {
        var a = MakePrice(id: 1, productId: 10, price: 1500m,
            from: new DateOnly(2026, 4, 1), to: null,
            fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));

        var b = MakePrice(id: 1, productId: 10, price: 1500m,
            from: new DateOnly(2026, 4, 1), to: null,
            fechaCreacion: new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc));

        a.Should().Be(b);
        (a == b).Should().BeTrue();
    }

    [Fact]
    public void Equality_DifferentId_AreNotEqual()
    {
        var a = MakePrice(id: 1);
        var b = MakePrice(id: 2);

        a.Should().NotBe(b);
    }

    // ── Immutability: record positional props are init-only (no regular set) ─

    [Fact]
    public void Record_PropertiesAreInitOnly_NotMutableSet()
    {
        // C# records expose init-only accessors, not regular set.
        // IsExternalInit attribute on the setter's return type distinguishes init from set.
        var type = typeof(ProductPrice);
        var mutableSetters = type.GetProperties()
            .Where(p =>
            {
                var setter = p.GetSetMethod(nonPublic: false);
                if (setter is null) return false;
                // init-only setters have a modreq on IsExternalInit in their signature;
                // a simple way to detect them: they are NOT init-only when
                // ReturnParameter.GetRequiredCustomModifiers() does NOT contain IsExternalInit.
                var modifiers = setter.ReturnParameter.GetRequiredCustomModifiers();
                bool isInitOnly = modifiers.Any(m => m.Name == "IsExternalInit");
                return !isInitOnly; // only flag non-init setters as mutable
            })
            .ToList();

        mutableSetters.Should().BeEmpty("ProductPrice must be immutable — all setters should be init-only.");
    }
}
