using System.Net.Sockets;
using MailKit;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery;
using ZB.MOM.WW.ScadaBridge.NotificationService;

namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests.Delivery;

/// <summary>
/// Task 12: Tests for the Email outbox delivery adapter — list/recipient/SMTP-config
/// resolution, SMTP send, and the three-way (Success / Permanent / Transient) outcome
/// classification.
/// </summary>
public class EmailNotificationDeliveryAdapterTests
{
    private readonly INotificationRepository _repository = Substitute.For<INotificationRepository>();
    private readonly ISmtpClientWrapper _smtpClient = Substitute.For<ISmtpClientWrapper>();

    private EmailNotificationDeliveryAdapter CreateAdapter()
    {
        return new EmailNotificationDeliveryAdapter(
            _repository,
            () => _smtpClient,
            NullLogger<EmailNotificationDeliveryAdapter>.Instance);
    }

    private static Notification MakeNotification(string listName = "ops-team")
    {
        return new Notification(
            Guid.NewGuid().ToString(),
            NotificationType.Email,
            listName,
            "Subject",
            "Body",
            "site-1");
    }

    private void SetupHappyPath()
    {
        var list = new NotificationList("ops-team") { Id = 1 };
        var recipients = new List<NotificationRecipient>
        {
            new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 },
            new("Bob", "bob@example.com") { Id = 2, NotificationListId = 1 }
        };
        var smtpConfig = new SmtpConfiguration("smtp.example.com", "basic", "noreply@example.com")
        {
            Id = 1, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
        };

        _repository.GetListByNameAsync("ops-team").Returns(list);
        _repository.GetRecipientsByListIdAsync(1).Returns(recipients);
        _repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration> { smtpConfig });
    }

    [Fact]
    public void Type_IsEmail()
    {
        Assert.Equal(NotificationType.Email, CreateAdapter().Type);
    }

    [Fact]
    public async Task Deliver_HappyPath_ReturnsSuccessWithResolvedTargets()
    {
        SetupHappyPath();
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.Success, outcome.Result);
        Assert.NotNull(outcome.ResolvedTargets);
        Assert.Contains("alice@example.com", outcome.ResolvedTargets);
        Assert.Contains("bob@example.com", outcome.ResolvedTargets);
        Assert.Null(outcome.Error);
    }

    [Fact]
    public async Task Deliver_ListNotFound_ReturnsPermanent()
    {
        _repository.GetListByNameAsync("missing").Returns((NotificationList?)null);
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification("missing"));

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("missing", outcome.Error);
        Assert.Contains("not found", outcome.Error);
    }

    [Fact]
    public async Task Deliver_NoRecipients_ReturnsPermanent()
    {
        var list = new NotificationList("ops-team") { Id = 1 };
        _repository.GetListByNameAsync("ops-team").Returns(list);
        _repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>());
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("recipient", outcome.Error);
    }

    [Fact]
    public async Task Deliver_NoSmtpConfig_ReturnsPermanent()
    {
        var list = new NotificationList("ops-team") { Id = 1 };
        _repository.GetListByNameAsync("ops-team").Returns(list);
        _repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>
        {
            new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 }
        });
        _repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration>());
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("SMTP", outcome.Error);
    }

    [Fact]
    public async Task Deliver_SmtpPermanentException_ReturnsPermanent()
    {
        SetupHappyPath();
        _smtpClient
            .SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new SmtpPermanentException("550 mailbox unavailable"));
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("mailbox unavailable", outcome.Error);
    }

    [Fact]
    public async Task Deliver_Smtp5xxCommandException_ReturnsPermanent()
    {
        SetupHappyPath();
        _smtpClient
            .SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new SmtpCommandException(
                SmtpErrorCode.RecipientNotAccepted, SmtpStatusCode.MailboxUnavailable, "rejected"));
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
    }

    [Fact]
    public async Task Deliver_SocketException_ReturnsTransient()
    {
        SetupHappyPath();
        _smtpClient
            .ConnectAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
                Arg.Any<int>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new SocketException((int)SocketError.ConnectionRefused));
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.TransientFailure, outcome.Result);
        Assert.NotNull(outcome.Error);
    }

    [Fact]
    public async Task Deliver_Smtp4xxCommandException_ReturnsTransient()
    {
        SetupHappyPath();
        _smtpClient
            .SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new SmtpCommandException(
                SmtpErrorCode.MessageNotAccepted, SmtpStatusCode.MailboxBusy, "try later"));
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.TransientFailure, outcome.Result);
    }

    [Fact]
    public async Task Deliver_UnclassifiedException_ReturnsPermanent()
    {
        SetupHappyPath();
        _smtpClient
            .SendAsync(Arg.Any<string>(), Arg.Any<IEnumerable<string>>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("token endpoint unreachable"));
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.NotNull(outcome.Error);
    }

    [Fact]
    public async Task Deliver_MalformedRecipientAddress_ReturnsPermanent()
    {
        const string malformed = "bad-recipient@";
        var list = new NotificationList("ops-team") { Id = 1 };
        _repository.GetListByNameAsync("ops-team").Returns(list);
        _repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>
        {
            new("Mallory", malformed) { Id = 1, NotificationListId = 1 }
        });
        _repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration>
        {
            new("smtp.example.com", "basic", "noreply@example.com")
            {
                Id = 1, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
            }
        });
        var adapter = CreateAdapter();

        var outcome = await adapter.DeliverAsync(MakeNotification());

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains(malformed, outcome.Error);
    }

    [Fact]
    public async Task Deliver_CancelledToken_ThrowsOperationCanceledException()
    {
        using var cts = new CancellationTokenSource();
        cts.Cancel();

        // List/recipient/SMTP-config resolution must succeed for any token so that
        // delivery proceeds far enough to observe the cancellation at the SMTP call.
        var list = new NotificationList("ops-team") { Id = 1 };
        _repository.GetListByNameAsync("ops-team", Arg.Any<CancellationToken>()).Returns(list);
        _repository.GetRecipientsByListIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(new List<NotificationRecipient>
            {
                new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 }
            });
        _repository.GetAllSmtpConfigurationsAsync(Arg.Any<CancellationToken>())
            .Returns(new List<SmtpConfiguration>
            {
                new("smtp.example.com", "basic", "noreply@example.com")
                {
                    Id = 1, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
                }
            });

        // The connect call honours the cancelled token, mirroring a real SMTP client.
        _smtpClient
            .ConnectAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
                Arg.Any<int>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new OperationCanceledException(cts.Token));
        var adapter = CreateAdapter();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => adapter.DeliverAsync(MakeNotification(), cts.Token));
    }

    [Fact]
    public async Task MultipleSmtpConfigs_LowestIdWins()
    {
        // Arch-review S7: when more than one SMTP configuration row exists the pick
        // must be deterministic (lowest Id), regardless of repository row order.
        var list = new NotificationList("ops-team") { Id = 1 };
        _repository.GetListByNameAsync("ops-team").Returns(list);
        _repository.GetRecipientsByListIdAsync(1).Returns(new List<NotificationRecipient>
        {
            new("Alice", "alice@example.com") { Id = 1, NotificationListId = 1 }
        });
        // Repository returns the higher-Id row FIRST — FirstOrDefault would pick it.
        _repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration>
        {
            new("smtp-seven.example.com", "basic", "noreply@example.com")
            {
                Id = 7, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
            },
            new("smtp-three.example.com", "basic", "noreply@example.com")
            {
                Id = 3, Port = 587, Credentials = "user:pass", TlsMode = "starttls"
            }
        });
        var adapter = CreateAdapter();

        await adapter.DeliverAsync(MakeNotification());

        // The lowest-Id row (3) must be the one connected to.
        await _smtpClient.Received().ConnectAsync(
            "smtp-three.example.com", Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
            Arg.Any<int>(), Arg.Any<CancellationToken>());
    }
}
