using System.Xml.Linq;
using ZB.MOM.WW.ScadaBridge.NotificationService.Ews;

namespace ZB.MOM.WW.ScadaBridge.NotificationService.Tests.Ews;

/// <summary>
/// Tests for the pure EWS <c>CreateItem</c> SOAP envelope builder: the fixed schema
/// sequence Exchange validates against, BCC-only recipients, and XML escaping of
/// operator-authored subject/body content.
/// </summary>
public class EwsSoapEnvelopeTests
{
    private static readonly XNamespace Soap = "http://schemas.xmlsoap.org/soap/envelope/";
    private static readonly XNamespace Types = "http://schemas.microsoft.com/exchange/services/2006/types";
    private static readonly XNamespace Messages = "http://schemas.microsoft.com/exchange/services/2006/messages";

    private static XDocument Build(
        string from = "scada@example.com",
        IReadOnlyList<string>? bcc = null,
        string subject = "Subject",
        string body = "Body")
        => XDocument.Parse(EwsSoapEnvelope.BuildCreateItem(from, bcc ?? ["one@example.com"], subject, body));

    [Fact]
    public void BuildCreateItem_SetsSendOnlyDispositionAndExchange2013RequestVersion()
    {
        var doc = Build();

        var createItem = doc.Descendants(Messages + "CreateItem").Single();
        Assert.Equal("SendOnly", (string?)createItem.Attribute("MessageDisposition"));

        var version = doc.Descendants(Types + "RequestServerVersion").Single();
        Assert.Equal("Exchange2013", (string?)version.Attribute("Version"));
        Assert.Single(doc.Descendants(Soap + "Header").Single().Elements());
    }

    [Fact]
    public void BuildCreateItem_EscapesMarkupInSubjectAndBody()
    {
        const string subject = "Alarm <HIGH> & \"critical\"";
        const string body = "value < 5 & tag=\"A\" > threshold";

        var doc = Build(subject: subject, body: body);

        // Round-tripping through the parser proves the content was escaped, not concatenated.
        Assert.Equal(subject, doc.Descendants(Types + "Subject").Single().Value);
        Assert.Equal(body, doc.Descendants(Types + "Body").Single().Value);
        Assert.Equal("Text", (string?)doc.Descendants(Types + "Body").Single().Attribute("BodyType"));
    }

    [Fact]
    public void BuildCreateItem_PutsEveryRecipientInBccAndNeverEmitsToRecipients()
    {
        string[] recipients = ["a@example.com", "b@example.com", "c@example.com"];

        var doc = Build(bcc: recipients);

        var bcc = doc.Descendants(Types + "BccRecipients").Single();
        var addresses = bcc.Elements(Types + "Mailbox")
            .Select(m => m.Element(Types + "EmailAddress")!.Value)
            .ToArray();
        Assert.Equal(recipients, addresses);

        // Recipient privacy is the whole reason for BCC-only: no To/Cc may leak the list.
        Assert.DoesNotContain(doc.Descendants(), e => e.Name.LocalName is "ToRecipients" or "CcRecipients");
    }

    [Fact]
    public void BuildCreateItem_EmitsFromMailbox()
    {
        var doc = Build(from: "scada-alerts@example.com");

        var from = doc.Descendants(Types + "From").Single();
        var mailbox = from.Elements(Types + "Mailbox").Single();
        Assert.Equal("scada-alerts@example.com", mailbox.Element(Types + "EmailAddress")!.Value);
    }

    [Fact]
    public void BuildCreateItem_OrdersMessageChildrenPerEwsSchemaSequence()
    {
        var doc = Build();

        // MessageType is an xs:sequence — any other order is ErrorSchemaValidation at the server.
        var message = doc.Descendants(Types + "Message").Single();
        Assert.Equal(
            new[] { "Subject", "Body", "BccRecipients", "From" },
            message.Elements().Select(e => e.Name.LocalName).ToArray());
    }

    [Fact]
    public void BuildCreateItem_StartsWithUtf8XmlDeclaration()
    {
        var xml = EwsSoapEnvelope.BuildCreateItem("a@example.com", ["b@example.com"], "s", "b");

        Assert.StartsWith("<?xml", xml, StringComparison.Ordinal);
        var declaration = xml[..xml.IndexOf("?>", StringComparison.Ordinal)];
        Assert.Contains("utf-8", declaration, StringComparison.OrdinalIgnoreCase);
    }
}
