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

namespace Books.Api.Tests.Domain;

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

    [Fact]
    public void Initiate_WithValidData_EmitsInitiatedEvent()
    {
        // Arrange
        var aggregate = new BankConnectionAggregate(BankConnectionId.New);

        // Act
        aggregate.Initiate("company-123", "Danske Bank", "auth-456", "https://callback.url", "state-789");

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

        var initiatedEvent = uncommittedEvents[0].AggregateEvent as BankConnectionInitiatedEvent;
        initiatedEvent.Should().NotBeNull();
        initiatedEvent!.CompanyId.Should().Be("company-123");
        initiatedEvent.AspspName.Should().Be("Danske Bank");
        initiatedEvent.AuthorizationId.Should().Be("auth-456");
        initiatedEvent.RedirectUrl.Should().Be("https://callback.url");
        initiatedEvent.State.Should().Be("state-789");
    }

    [Fact]
    public void Initiate_WithEmptyAspspName_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new BankConnectionAggregate(BankConnectionId.New);

        // Act
        var act = () => aggregate.Initiate("company-123", "  ", "auth-456", "https://callback.url", "state-789");

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

    [Fact]
    public void Initiate_WhenAlreadyInitiated_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Act
        var act = () => aggregate.Initiate("company-123", "Nordea", "auth-999", "https://other.url", "state-111");

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

    #endregion

    #region Establish Tests

    [Fact]
    public void Establish_WhenInitiated_EmitsEstablishedEvent()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();
        var accounts = new List<BankAccountInfo>
        {
            new("acc-1", "DK1234567890123456", "DKK", "Lønkonto"),
            new("acc-2", "DK9876543210987654", "DKK", "Opsparingskonto")
        };
        var validUntil = DateTimeOffset.UtcNow.AddDays(90);

        // Act
        aggregate.Establish("session-123", validUntil, accounts);

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

        var establishedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionEstablishedEvent;
        establishedEvent.Should().NotBeNull();
        establishedEvent!.SessionId.Should().Be("session-123");
        establishedEvent.ValidUntil.Should().Be(validUntil);
        establishedEvent.Accounts.Should().HaveCount(2);
    }

    [Fact]
    public void Establish_WhenNotInitiated_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new BankConnectionAggregate(BankConnectionId.New);
        var accounts = new List<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };

        // Act
        var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts);

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

    [Fact]
    public void Establish_WithEmptySessionId_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();
        var accounts = new List<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };

        // Act
        var act = () => aggregate.Establish("  ", DateTimeOffset.UtcNow.AddDays(90), accounts);

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

    [Fact]
    public void Establish_WithNoAccounts_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();
        var accounts = new List<BankAccountInfo>();

        // Act
        var act = () => aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts);

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

    [Fact]
    public void Establish_WhenAlreadyEstablished_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();
        var accounts = new List<BankAccountInfo> { new("acc-1", "DK1234", "DKK", null) };

        // Act
        var act = () => aggregate.Establish("session-456", DateTimeOffset.UtcNow.AddDays(90), accounts);

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

    #endregion

    #region Fail Tests

    [Fact]
    public void Fail_WhenInitiated_EmitsFailedEvent()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Act
        aggregate.Fail("User cancelled authorization");

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

        var failedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionFailedEvent;
        failedEvent.Should().NotBeNull();
        failedEvent!.Reason.Should().Be("User cancelled authorization");
    }

    [Fact]
    public void Fail_WhenNotInitiated_ThrowsDomainException()
    {
        // Arrange
        var aggregate = new BankConnectionAggregate(BankConnectionId.New);

        // Act
        var act = () => aggregate.Fail("Some reason");

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

    #endregion

    #region Disconnect Tests

    [Fact]
    public void Disconnect_WhenEstablished_EmitsDisconnectedEvent()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();

        // Act
        aggregate.Disconnect("User requested disconnection");

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().HaveCount(3); // Initiated + Established + Disconnected

        var disconnectedEvent = uncommittedEvents[2].AggregateEvent as BankConnectionDisconnectedEvent;
        disconnectedEvent.Should().NotBeNull();
        disconnectedEvent!.Reason.Should().Be("User requested disconnection");
    }

    [Fact]
    public void Disconnect_WhenInitiated_EmitsDisconnectedEvent()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Act
        aggregate.Disconnect("User cancelled flow");

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

        var disconnectedEvent = uncommittedEvents[1].AggregateEvent as BankConnectionDisconnectedEvent;
        disconnectedEvent.Should().NotBeNull();
        disconnectedEvent!.Reason.Should().Be("User cancelled flow");
    }

    [Fact]
    public void Disconnect_WhenAlreadyDisconnected_Idempotent_DoesNothing()
    {
        // Arrange
        var aggregate = CreateDisconnectedConnection();
        var initialEventCount = aggregate.UncommittedEvents.Count();

        // Act
        aggregate.Disconnect("Trying again");

        // Assert
        aggregate.UncommittedEvents.Count().Should().Be(initialEventCount);
    }

    #endregion

    #region LinkBankAccount Tests

    [Fact]
    public void LinkBankAccount_WhenEstablished_EmitsLinkedEvent()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();
        var importDate = new DateOnly(2023, 1, 1);

        // Act
        aggregate.LinkBankAccount("acc-1", "ledger-1000", importDate);

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        var linkedEvent = uncommittedEvents.Last().AggregateEvent as BankAccountLinkedEvent;
        
        linkedEvent.Should().NotBeNull();
        linkedEvent!.BankAccountId.Should().Be("acc-1");
        linkedEvent.LinkedAccountId.Should().Be("ledger-1000");
        linkedEvent.ImportFromDate.Should().Be(importDate);
    }

    [Fact]
    public void LinkBankAccount_WhenNotEstablished_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Act
        var act = () => aggregate.LinkBankAccount("acc-1", "ledger-1000");

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

    [Fact]
    public void LinkBankAccount_WithUnknownAccountId_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();

        // Act
        var act = () => aggregate.LinkBankAccount("unknown-acc", "ledger-1000");

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

    #endregion

    #region ReInitiate Tests

    [Fact]
    public void ReInitiate_WhenDisconnected_EmitsReInitiatedEvent()
    {
        // Arrange
        var aggregate = CreateDisconnectedConnection();

        // Act
        aggregate.ReInitiate("new-auth", "url", "new-state");

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        var event_ = uncommittedEvents.Last().AggregateEvent as BankConnectionReInitiatedEvent;

        event_.Should().NotBeNull();
        event_!.AuthorizationId.Should().Be("new-auth");
        event_.State.Should().Be("new-state");
    }

    [Fact]
    public void ReInitiate_WhenActive_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();

        // Act
        var act = () => aggregate.ReInitiate("auth", "url", "state");

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

    #endregion

    #region Archive Tests

    [Fact]
    public void Archive_WhenDisconnected_EmitsArchivedEvent()
    {
        // Arrange
        var aggregate = CreateDisconnectedConnection();

        // Act
        aggregate.Archive();

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        var event_ = uncommittedEvents.Last().AggregateEvent as BankConnectionArchivedEvent;

        event_.Should().NotBeNull();
    }

    [Fact]
    public void Archive_WhenActive_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();

        // Act
        var act = () => aggregate.Archive();

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

    #endregion

    #region Refresh Tests

    [Fact]
    public void Refresh_WhenEstablished_EmitsRefreshedEvent()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();
        var newValidUntil = DateTimeOffset.UtcNow.AddDays(180);

        // Act
        aggregate.Refresh("new-session-456", newValidUntil);

        // Assert
        var uncommittedEvents = aggregate.UncommittedEvents.ToList();
        uncommittedEvents.Should().HaveCount(3); // Initiated + Established + Refreshed

        var refreshedEvent = uncommittedEvents[2].AggregateEvent as BankConnectionRefreshedEvent;
        refreshedEvent.Should().NotBeNull();
        refreshedEvent!.NewSessionId.Should().Be("new-session-456");
        refreshedEvent.ValidUntil.Should().Be(newValidUntil);
    }

    [Fact]
    public void Refresh_WhenNotEstablished_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Act
        var act = () => aggregate.Refresh("new-session-456", DateTimeOffset.UtcNow.AddDays(90));

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

    [Fact]
    public void Refresh_WhenDisconnected_ThrowsDomainException()
    {
        // Arrange
        var aggregate = CreateDisconnectedConnection();

        // Act
        var act = () => aggregate.Refresh("new-session-456", DateTimeOffset.UtcNow.AddDays(90));

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

    #endregion

    #region IsActive Property Tests

    [Fact]
    public void IsActive_WhenEstablishedAndNotExpired_ReturnsTrue()
    {
        // Arrange
        var aggregate = CreateEstablishedConnection();

        // Assert
        aggregate.IsActive.Should().BeTrue();
    }

    [Fact]
    public void IsActive_WhenNotEstablished_ReturnsFalse()
    {
        // Arrange
        var aggregate = CreateInitiatedConnection();

        // Assert
        aggregate.IsActive.Should().BeFalse();
    }

    [Fact]
    public void IsActive_WhenDisconnected_ReturnsFalse()
    {
        // Arrange
        var aggregate = CreateDisconnectedConnection();

        // Assert
        aggregate.IsActive.Should().BeFalse();
    }

    #endregion

    #region Helper Methods

    private static BankConnectionAggregate CreateInitiatedConnection()
    {
        var aggregate = new BankConnectionAggregate(BankConnectionId.New);
        aggregate.Initiate("company-123", "Danske Bank", "auth-456", "https://callback.url", "state-789");
        return aggregate;
    }

    private static BankConnectionAggregate CreateEstablishedConnection()
    {
        var aggregate = CreateInitiatedConnection();
        var accounts = new List<BankAccountInfo>
        {
            new("acc-1", "DK1234567890123456", "DKK", "Lønkonto")
        };
        aggregate.Establish("session-123", DateTimeOffset.UtcNow.AddDays(90), accounts);
        return aggregate;
    }

    private static BankConnectionAggregate CreateDisconnectedConnection()
    {
        var aggregate = CreateEstablishedConnection();
        aggregate.Disconnect("User requested disconnection");
        return aggregate;
    }

    #endregion
}
