using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using SIGCM2.Domain.Entities;

namespace SIGCM2.Application.Tests.Domain;

/// <summary>
/// PRD-002 — Domain entity tests for Product.
/// Validates factory method, mutation methods, and validation rules.
/// </summary>
public class ProductTests
{
    private static readonly FakeTimeProvider _time =
        new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));

    private static Product ValidProduct(int? rubroId = null, int? priceDurationDays = null)
        => Product.ForCreation(
            nombre: "Clasificado Estándar",
            medioId: 1,
            productTypeId: 2,
            rubroId: rubroId,
            basePrice: 100.50m,
            priceDurationDays: priceDurationDays,
            timeProvider: _time);

    // ── R1-S1: ForCreation happy path ─────────────────────────────────────────

    [Fact]
    public void ForCreation_ValidData_ReturnsEntityWithDefaults()
    {
        var p = ValidProduct();

        p.IsActive.Should().BeTrue();
        p.Id.Should().Be(0);
        p.FechaCreacion.Should().Be(_time.GetUtcNow().UtcDateTime);
        p.FechaModificacion.Should().BeNull();
        p.Nombre.Should().Be("Clasificado Estándar");
        p.MedioId.Should().Be(1);
        p.ProductTypeId.Should().Be(2);
        p.RubroId.Should().BeNull();
        p.BasePrice.Should().Be(100.50m);
        p.PriceDurationDays.Should().BeNull();
    }

    // ── R1-S2: Nombre vacío ────────────────────────────────────────────────────

    [Fact]
    public void ForCreation_EmptyNombre_ThrowsArgumentException()
    {
        var act = () => Product.ForCreation("", 1, 2, null, 10m, null, _time);

        act.Should().Throw<ArgumentException>()
            .WithMessage("*Nombre*");
    }

    // ── R1-S3: Nombre solo espacios ────────────────────────────────────────────

    [Fact]
    public void ForCreation_WhitespaceNombre_ThrowsArgumentException()
    {
        var act = () => Product.ForCreation("   ", 1, 2, null, 10m, null, _time);

        act.Should().Throw<ArgumentException>();
    }

    // ── R1-S4: BasePrice negativo ──────────────────────────────────────────────

    [Fact]
    public void ForCreation_NegativeBasePrice_ThrowsArgumentException()
    {
        var act = () => Product.ForCreation("Test", 1, 2, null, -1m, null, _time);

        act.Should().Throw<ArgumentException>()
            .WithMessage("*basePrice*");
    }

    // ── R1-S5: BasePrice = 0 es válido ─────────────────────────────────────────

    [Fact]
    public void ForCreation_ZeroBasePrice_DoesNotThrow()
    {
        var act = () => Product.ForCreation("Test", 1, 2, null, 0m, null, _time);

        act.Should().NotThrow();
    }

    // ── R1-S6: PriceDurationDays = 0 ──────────────────────────────────────────

    [Fact]
    public void ForCreation_ZeroPriceDurationDays_ThrowsArgumentException()
    {
        var act = () => Product.ForCreation("Test", 1, 2, null, 10m, 0, _time);

        act.Should().Throw<ArgumentException>()
            .WithMessage("*priceDurationDays*");
    }

    // ── R1-S7: PriceDurationDays negativo ─────────────────────────────────────

    [Fact]
    public void ForCreation_NegativePriceDurationDays_ThrowsArgumentException()
    {
        var act = () => Product.ForCreation("Test", 1, 2, null, 10m, -5, _time);

        act.Should().Throw<ArgumentException>();
    }

    // ── R1-S8: PriceDurationDays válido ───────────────────────────────────────

    [Fact]
    public void ForCreation_ValidPriceDurationDays_SetsValue()
    {
        var p = Product.ForCreation("Test", 1, 2, null, 10m, 30, _time);

        p.PriceDurationDays.Should().Be(30);
    }

    // ── R1-S9: WithDeactivated ─────────────────────────────────────────────────

    [Fact]
    public void WithDeactivated_ActiveProduct_ReturnsInactiveWithModDate()
    {
        var product = ValidProduct();
        var tp2 = new FakeTimeProvider(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));

        var deactivated = product.WithDeactivated(tp2);

        deactivated.IsActive.Should().BeFalse();
        deactivated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
        // Immutability: original unchanged
        product.IsActive.Should().BeTrue();
    }

    // ── R1-S10: WithDeactivated idempotente ───────────────────────────────────

    [Fact]
    public void WithDeactivated_AlreadyInactive_ReturnsInactiveNoProblem()
    {
        var product = ValidProduct();
        var deactivated = product.WithDeactivated(_time);

        var act = () => deactivated.WithDeactivated(_time);

        act.Should().NotThrow();
        act().IsActive.Should().BeFalse();
    }

    // ── R1-S11: WithUpdated ───────────────────────────────────────────────────

    [Fact]
    public void WithUpdated_ValidFields_ReturnsNewInstanceWithUpdatedValues()
    {
        var product = ValidProduct();
        var tp2 = new FakeTimeProvider(new DateTimeOffset(2026, 4, 20, 12, 0, 0, TimeSpan.Zero));

        var updated = product.WithUpdated("Nuevo Nombre", rubroId: null, basePrice: 200m, priceDurationDays: 15, tp2);

        updated.Nombre.Should().Be("Nuevo Nombre");
        updated.BasePrice.Should().Be(200m);
        updated.PriceDurationDays.Should().Be(15);
        updated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
        // Immutability: original unchanged
        product.Nombre.Should().Be("Clasificado Estándar");
        product.BasePrice.Should().Be(100.50m);
    }

    // ── Immutability: With* return new instances ──────────────────────────────

    [Fact]
    public void WithRenamed_ReturnsNewInstance()
    {
        var p = ValidProduct();
        var renamed = p.WithRenamed("Nuevo", _time);

        renamed.Should().NotBeSameAs(p);
        renamed.Nombre.Should().Be("Nuevo");
    }

    [Fact]
    public void WithUpdatedPrice_ReturnsNewInstance()
    {
        var p = ValidProduct();
        var updated = p.WithUpdatedPrice(999m, null, _time);

        updated.Should().NotBeSameAs(p);
        updated.BasePrice.Should().Be(999m);
    }

    [Fact]
    public void WithUpdatedCategory_ReturnsNewInstance()
    {
        var p = ValidProduct();
        var updated = p.WithUpdatedCategory(5, _time);

        updated.Should().NotBeSameAs(p);
        updated.RubroId.Should().Be(5);
    }

    // ── MedioId and ProductTypeId are immutable ───────────────────────────────

    [Fact]
    public void Product_HasNoMethodToChangeMedioId()
    {
        var type = typeof(Product);
        var methods = type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);

        methods.Should().NotContain(m => m.Name.Contains("Medio") && m.Name.StartsWith("With"));
    }

    [Fact]
    public void Product_HasNoMethodToChangeProductTypeId()
    {
        var type = typeof(Product);
        var methods = type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);

        methods.Should().NotContain(m => m.Name.Contains("ProductType") && m.Name.StartsWith("With"));
    }
}
