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;
using ZB.MOM.WW.ScadaBridge.NotificationService.Ews;

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

/// <summary>
/// Task 5: Tests for the EWS transport branch of the Email outbox delivery adapter —
/// the transport parse, the EWS config-shape validation (each failure permanent and
/// short-circuiting before the sender is touched), the request the sender receives,
/// the typed transient/permanent outcome mapping, and the proof that an unset or
/// "Smtp" transport still takes the untouched SMTP path.
/// </summary>
public class EmailNotificationDeliveryAdapterEwsTests
{
    private const string Endpoint = "https://ews.example.test/ews/exchange.asmx";

    private readonly INotificationRepository _repository = Substitute.For<INotificationRepository>();
    private readonly ISmtpClientWrapper _smtpClient = Substitute.For<ISmtpClientWrapper>();
    private readonly IEwsMailSender _ewsSender = Substitute.For<IEwsMailSender>();

    private EmailNotificationDeliveryAdapter CreateAdapter(bool withEwsSender = true)
    {
        return new EmailNotificationDeliveryAdapter(
            _repository,
            () => _smtpClient,
            NullLogger<EmailNotificationDeliveryAdapter>.Instance,
            tokenService: null,
            options: null,
            ewsMailSender: withEwsSender ? _ewsSender : null);
    }

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

    /// <summary>
    /// Wires a resolvable list with two recipients plus a single configuration row shaped
    /// by the caller — the one knob every case below turns.
    /// </summary>
    private void SetupWithConfig(SmtpConfiguration config)
    {
        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 }
        };

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

    private static SmtpConfiguration EwsConfig(
        string host = Endpoint,
        string authType = "basic",
        string? credentials = @"dom\svc:pw",
        string transport = "Ews")
    {
        return new SmtpConfiguration(host, authType, "noreply@example.com")
        {
            Id = 1,
            Transport = transport,
            Credentials = credentials,
            ConnectionTimeoutSeconds = 45
        };
    }

    private static SmtpConfiguration SmtpConfig(string? transport)
    {
        return new SmtpConfiguration("smtp.example.com", "basic", "noreply@example.com")
        {
            Id = 1, Port = 587, Credentials = "user:pass", TlsMode = "starttls", Transport = transport
        };
    }

    private EwsSendRequest CapturedRequest()
    {
        var call = _ewsSender.ReceivedCalls().Single();
        return (EwsSendRequest)call.GetArguments()[0]!;
    }

    private async Task AssertSenderUntouched()
    {
        await _ewsSender.DidNotReceive().SendAsync(
            Arg.Any<EwsSendRequest>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Deliver_EwsTransport_SendsThroughEwsSenderAndReturnsSuccess()
    {
        SetupWithConfig(EwsConfig());
        var adapter = CreateAdapter();

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

        var request = CapturedRequest();
        Assert.Equal(new Uri(Endpoint), request.Endpoint);
        Assert.Equal(@"dom\svc", request.Username);
        Assert.Equal("pw", request.Password);
        Assert.Equal("noreply@example.com", request.FromAddress);
        Assert.Equal(new[] { "alice@example.com", "bob@example.com" }, request.BccRecipients);
        Assert.Equal("Subject", request.Subject);
        Assert.Equal("Body", request.Body);
        Assert.Equal(45, request.TimeoutSeconds);

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

        // The EWS branch must never open an SMTP connection.
        await _smtpClient.DidNotReceive().ConnectAsync(
            Arg.Any<string>(), Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
            Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Deliver_PasswordContainsColon_SplitsOnFirstColonOnly()
    {
        SetupWithConfig(EwsConfig(credentials: @"dom\svc:p:w"));
        var adapter = CreateAdapter();

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

        var request = CapturedRequest();
        Assert.Equal(@"dom\svc", request.Username);
        Assert.Equal("p:w", request.Password);
        Assert.Equal(DeliveryResult.Success, outcome.Result);
    }

    [Fact]
    public async Task Deliver_EwsTransientException_ReturnsTransient()
    {
        SetupWithConfig(EwsConfig());
        _ewsSender.SendAsync(Arg.Any<EwsSendRequest>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new EwsTransientException("503 server busy"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.TransientFailure, outcome.Result);
        Assert.Contains("server busy", outcome.Error);
    }

    [Fact]
    public async Task Deliver_EwsPermanentException_ReturnsPermanent()
    {
        SetupWithConfig(EwsConfig());
        _ewsSender.SendAsync(Arg.Any<EwsSendRequest>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new EwsPermanentException("401 unauthorized"));
        var adapter = CreateAdapter();

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

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

    [Fact]
    public async Task Deliver_UnclassifiedSenderException_ReturnsPermanentAndRedactsCredential()
    {
        SetupWithConfig(EwsConfig(credentials: @"dom\svc:super-secret-password"));
        _ewsSender.SendAsync(Arg.Any<EwsSendRequest>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException(
                @"boom for dom\svc:super-secret-password"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.DoesNotContain("super-secret-password", outcome.Error);
    }

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

        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> { EwsConfig() });
        _ewsSender.SendAsync(Arg.Any<EwsSendRequest>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new OperationCanceledException(cts.Token));
        var adapter = CreateAdapter();

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

    [Fact]
    public async Task Deliver_NonHttpsEndpoint_ReturnsPermanentWithoutCallingSender()
    {
        SetupWithConfig(EwsConfig(host: "http://ews.example.test/ews/exchange.asmx"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("https", outcome.Error);
        await AssertSenderUntouched();
    }

    [Fact]
    public async Task Deliver_NonAbsoluteEndpoint_ReturnsPermanentWithoutCallingSender()
    {
        SetupWithConfig(EwsConfig(host: "ews.example.test"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("ews.example.test", outcome.Error);
        await AssertSenderUntouched();
    }

    [Fact]
    public async Task Deliver_NonBasicAuthType_ReturnsPermanentWithoutCallingSender()
    {
        SetupWithConfig(EwsConfig(authType: "oauth2"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("Basic", outcome.Error, StringComparison.OrdinalIgnoreCase);
        await AssertSenderUntouched();
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("no-colon-here")]
    [InlineData(":pw")]
    [InlineData(@"dom\svc:")]
    public async Task Deliver_MalformedCredentials_ReturnsPermanentWithoutCallingSender(
        string? credentials)
    {
        SetupWithConfig(EwsConfig(credentials: credentials));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("username:password", outcome.Error);
        await AssertSenderUntouched();
    }

    [Fact]
    public async Task Deliver_UnknownTransport_ReturnsPermanentWithoutCallingSender()
    {
        SetupWithConfig(EwsConfig(transport: "Graph"));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.PermanentFailure, outcome.Result);
        Assert.Contains("Graph", outcome.Error);
        await AssertSenderUntouched();
        await _smtpClient.DidNotReceive().ConnectAsync(
            Arg.Any<string>(), Arg.Any<int>(), Arg.Any<SmtpTlsMode>(),
            Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Deliver_NoEwsSenderRegistered_ReturnsPermanent()
    {
        SetupWithConfig(EwsConfig());
        var adapter = CreateAdapter(withEwsSender: false);

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

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

    [Theory]
    [InlineData(null)]
    [InlineData("Smtp")]
    public async Task Deliver_SmtpOrUnsetTransport_TakesSmtpPath(string? transport)
    {
        SetupWithConfig(SmtpConfig(transport));
        var adapter = CreateAdapter();

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

        Assert.Equal(DeliveryResult.Success, outcome.Result);
        await _smtpClient.Received().ConnectAsync(
            "smtp.example.com", 587, SmtpTlsMode.StartTls,
            Arg.Any<int>(), Arg.Any<CancellationToken>());
        await AssertSenderUntouched();
    }
}
