using Books.Api.Domain;
using Books.Api.Domain.JournalEntryDrafts;
using Books.Api.Domain.JournalEntryDrafts.Events;
using AwesomeAssertions;

namespace Books.Api.Tests.Domain;

/// <summary>
/// Unit tests for JournalEntryDraftAggregate domain logic.
/// Tests aggregate behavior without EventFlow infrastructure.
/// </summary>
[Trait("Category", "Unit")]
public class JournalEntryDraftAggregateTests
{
    #region Create Tests

    [Fact]
    public void Create_WithValidData_EmitsCreatedEvent()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        aggregate.Create("company-123", "Test Draft", "user@example.com", "K-0001");

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().ContainSingle();

        var createdEvent = uncommittedEvents[0].AggregateEvent as JournalEntryDraftCreatedEvent;
        createdEvent.Should().NotBeNull();
        createdEvent!.CompanyId.Should().Be("company-123");
        createdEvent.Name.Should().Be("Test Draft");
        createdEvent.CreatedBy.Should().Be("user@example.com");
        createdEvent.VoucherNumber.Should().Be("K-0001");
    }

    [Fact]
    public void Create_TrimsWhitespace()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        aggregate.Create("  company-123  ", "  Trimmed Name  ", "user@example.com", "  K-0002  ");

        // Assert
        var createdEvent = aggregate.UncommittedEvents.First().AggregateEvent as JournalEntryDraftCreatedEvent;
        createdEvent!.CompanyId.Should().Be("company-123");
        createdEvent.Name.Should().Be("Trimmed Name");
        createdEvent.VoucherNumber.Should().Be("K-0002");
    }

    [Fact]
    public void Create_WithEmptyName_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        var act = () => aggregate.Create("company-123", "   ", "user@example.com", "K-0001");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_NAME_REQUIRED");
    }

    [Fact]
    public void Create_WithEmptyCompanyId_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        var act = () => aggregate.Create("", "Test Draft", "user@example.com", "K-0001");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "COMPANY_ID_REQUIRED");
    }

    [Fact]
    public void Create_WithEmptyCreatedBy_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        var act = () => aggregate.Create("company-123", "Test Draft", "  ", "K-0001");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "CREATED_BY_REQUIRED");
    }

    [Fact]
    public void Create_WithEmptyVoucherNumber_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        var act = () => aggregate.Create("company-123", "Test Draft", "user@example.com", "  ");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "VOUCHER_NUMBER_REQUIRED");
    }

    [Fact]
    public void Create_WhenAlreadyCreated_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());
        aggregate.Create("company-123", "First Draft", "user@example.com", "K-0001");

        // Act
        var act = () => aggregate.Create("company-123", "Second Draft", "user@example.com", "K-0002");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_EXISTS");
    }

    #endregion

    #region Update Tests

    [Fact]
    public void Update_WhenActive_EmitsUpdatedEvent()
    {
        // Arrange
        var aggregate = CreateActiveDraft();
        var lines = new List<DraftLine>
        {
            new(1, "account-1", 1000m, 0m, "Debet"),
            new(2, "account-2", 0m, 1000m, "Kredit")
        };

        // Act
        aggregate.Update("New Name", DateOnly.FromDateTime(DateTime.Today), "Description", "fiscalyear-1", lines);

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().HaveCount(2); // Created + Updated

        var updatedEvent = uncommittedEvents[1].AggregateEvent as JournalEntryDraftUpdatedEvent;
        updatedEvent.Should().NotBeNull();
        updatedEvent!.Name.Should().Be("New Name");
        updatedEvent.DocumentDate.Should().Be(DateOnly.FromDateTime(DateTime.Today));
        updatedEvent.Description.Should().Be("Description");
        updatedEvent.FiscalYearId.Should().Be("fiscalyear-1");
        updatedEvent.Lines.Should().HaveCount(2);
    }

    [Fact]
    public void Update_WithVatCode_IncludesVatCodeInEvent()
    {
        // Arrange
        var aggregate = CreateActiveDraft();
        var lines = new List<DraftLine>
        {
            new(1, "account-1", 1000m, 0m, "Salg", "U25"),
            new(2, "account-2", 0m, 1000m, "Moms", "I25")
        };

        // Act
        aggregate.Update("Draft with VAT", DateOnly.FromDateTime(DateTime.Today), "Description", "fiscalyear-1", lines);

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        var updatedEvent = uncommittedEvents[1].AggregateEvent as JournalEntryDraftUpdatedEvent;
        updatedEvent.Should().NotBeNull();
        updatedEvent!.Lines[0].VatCode.Should().Be("U25");
        updatedEvent.Lines[1].VatCode.Should().Be("I25");
    }

    [Fact]
    public void Update_WithInvalidVatCode_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateActiveDraft();
        var lines = new List<DraftLine>
        {
            new(1, "account-1", 1000m, 0m, "Test", "INVALID_CODE")
        };

        // Act
        var act = () => aggregate.Update("Name", null, null, null, lines);

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "INVALID_VAT_CODE");
    }

    [Fact]
    public void Update_WhenNotCreated_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());

        // Act
        var act = () => aggregate.Update("Name", null, null, null, []);

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_NOT_FOUND");
    }

    [Fact]
    public void Update_WhenPosted_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreatePostedDraft();

        // Act
        var act = () => aggregate.Update("New Name", null, null, null, []);

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_POSTED");
    }

    [Fact]
    public void Update_WhenDiscarded_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateDiscardedDraft();

        // Act
        var act = () => aggregate.Update("New Name", null, null, null, []);

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_DISCARDED");
    }

    #endregion

    #region MarkPosted Tests

    [Fact]
    public void MarkPosted_WhenActive_EmitsPostedEvent()
    {
        // Arrange
        var aggregate = CreateActiveDraftWithLines();

        // Act
        aggregate.MarkPosted("transaction-123", "user@example.com");

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().HaveCount(3); // Created + Updated + Posted

        var postedEvent = uncommittedEvents[2].AggregateEvent as JournalEntryDraftPostedEvent;
        postedEvent.Should().NotBeNull();
        postedEvent!.TransactionId.Should().Be("transaction-123");
        postedEvent.PostedBy.Should().Be("user@example.com");
        postedEvent.PostedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void MarkPosted_WithEmptyTransactionId_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateActiveDraftWithLines();

        // Act
        var act = () => aggregate.MarkPosted("  ", "user@example.com");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "TRANSACTION_ID_REQUIRED");
    }

    [Fact]
    public void MarkPosted_WithEmptyPostedBy_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateActiveDraftWithLines();

        // Act
        var act = () => aggregate.MarkPosted("transaction-123", "");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "POSTED_BY_REQUIRED");
    }

    [Fact]
    public void MarkPosted_WhenAlreadyPosted_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreatePostedDraft();

        // Act
        var act = () => aggregate.MarkPosted("transaction-456", "user@example.com");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_POSTED");
    }

    #endregion

    #region Discard Tests

    [Fact]
    public void Discard_WhenActive_EmitsDiscardedEvent()
    {
        // Arrange
        var aggregate = CreateActiveDraft();

        // Act
        aggregate.Discard("user@example.com");

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().HaveCount(2); // Created + Discarded

        var discardedEvent = uncommittedEvents[1].AggregateEvent as JournalEntryDraftDiscardedEvent;
        discardedEvent.Should().NotBeNull();
        discardedEvent!.DiscardedBy.Should().Be("user@example.com");
    }

    [Fact]
    public void Discard_WithEmptyDiscardedBy_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateActiveDraft();

        // Act
        var act = () => aggregate.Discard("   ");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DISCARDED_BY_REQUIRED");
    }

    [Fact]
    public void Discard_WhenAlreadyDiscarded_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateDiscardedDraft();

        // Act
        var act = () => aggregate.Discard("user@example.com");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_DISCARDED");
    }

    [Fact]
    public void Discard_WhenPosted_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreatePostedDraft();

        // Act
        var act = () => aggregate.Discard("user@example.com");

        // Assert
        act.Should().Throw<DomainException>()
            .Where(e => e.Code == "DRAFT_ALREADY_POSTED");
    }

    #endregion

    #region Helper Methods

    private static JournalEntryDraftAggregate CreateActiveDraft()
    {
        var aggregate = new JournalEntryDraftAggregate(JournalEntryDraftId.New());
        aggregate.Create("company-123", "Test Draft", "user@example.com", "K-0001");
        return aggregate;
    }

    private static JournalEntryDraftAggregate CreateActiveDraftWithLines()
    {
        var aggregate = CreateActiveDraft();
        var lines = new List<DraftLine>
        {
            new(1, "account-1", 1000m, 0m, "Debet"),
            new(2, "account-2", 0m, 1000m, "Kredit")
        };
        aggregate.Update("Test Draft", DateOnly.FromDateTime(DateTime.Today), "Description", "fiscalyear-1", lines);
        return aggregate;
    }

    private static JournalEntryDraftAggregate CreatePostedDraft()
    {
        var aggregate = CreateActiveDraftWithLines();
        aggregate.MarkPosted("transaction-123", "user@example.com");
        return aggregate;
    }

    private static JournalEntryDraftAggregate CreateDiscardedDraft()
    {
        var aggregate = CreateActiveDraft();
        aggregate.Discard("user@example.com");
        return aggregate;
    }

    #endregion
}
