using System.CommandLine;
using ZB.MOM.WW.ScadaBridge.CLI;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands;

/// <summary>
/// Tests for the SMS surface of the <c>notification</c> command group (SMS Notifications S6):
/// the channel-aware <c>--type</c> / <c>--phones</c> flags on <c>notification list create|update</c>,
/// and the new <c>notification sms list|update</c> group. Pins the validation rules, the
/// per-channel command construction, and that the Twilio Auth Token is never echoed back.
/// </summary>
public class NotificationSmsCommandTests
{
    private static readonly Option<string> Url = new("--url") { Recursive = true };
    private static readonly Option<string> Username = new("--username") { Recursive = true };
    private static readonly Option<string> Password = new("--password") { Recursive = true };
    private static readonly Option<string> Format = CliOptions.CreateFormatOption();

    private static Command Notification() => NotificationCommands.Build(Url, Format, Username, Password);

    private static Command ListCreate() =>
        Notification().Subcommands.Single(c => c.Name == "create");

    private static Command ListUpdate() =>
        Notification().Subcommands.Single(c => c.Name == "update");

    private static Command SmsGroup() =>
        Notification().Subcommands.Single(c => c.Name == "sms");

    private static Command SmsUpdate() =>
        SmsGroup().Subcommands.Single(c => c.Name == "update");

    // System.CommandLine's Parse takes a string[] (no params overload here), so route
    // every invocation through a helper that wraps the variadic args into an array.
    private static System.CommandLine.ParseResult Parse(Command command, params string[] args)
        => command.Parse(args);

    // ---- list create/update: --type + --phones ----------------------------

    [Fact]
    public void ListCreate_SmsTypeWithPhones_BuildsSmsCommandWithRecipientPhones()
    {
        var parse = Parse(ListCreate(), 
            "--name", "Ops SMS", "--type", "sms", "--phones", "+15551230000,+15551230001");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);

        Assert.Equal("Ops SMS", cmd.Name);
        Assert.Equal(NotificationType.Sms, cmd.Type);
        Assert.Equal(new[] { "+15551230000", "+15551230001" }, cmd.RecipientPhones);
        Assert.Empty(cmd.RecipientEmails);
    }

    [Fact]
    public void ListCreate_TypeIsCaseInsensitive()
    {
        var parse = Parse(ListCreate(), 
            "--name", "Ops SMS", "--type", "SMS", "--phones", "+15551230000");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);
        Assert.Equal(NotificationType.Sms, cmd.Type);
    }

    [Fact]
    public void ListCreate_DefaultType_IsEmail()
    {
        var parse = Parse(ListCreate(), "--name", "Ops", "--emails", "ops@example.com");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);

        Assert.Equal(NotificationType.Email, cmd.Type);
        Assert.Equal(new[] { "ops@example.com" }, cmd.RecipientEmails);
        Assert.Null(cmd.RecipientPhones);
    }

    [Fact]
    public void ListCreate_EmailTypeWithPhones_Throws()
    {
        var parse = Parse(ListCreate(), 
            "--name", "Ops", "--type", "email", "--emails", "ops@example.com", "--phones", "+15551230000");

        var ex = Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildCreateNotificationListCommand(parse));
        Assert.Contains("--phones", ex.Message);
    }

    [Fact]
    public void ListCreate_SmsTypeWithoutPhones_Throws()
    {
        var parse = Parse(ListCreate(), "--name", "Ops SMS", "--type", "sms");

        var ex = Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildCreateNotificationListCommand(parse));
        Assert.Contains("--phones", ex.Message);
    }

    [Fact]
    public void ListCreate_EmailTypeWithoutEmails_Throws()
    {
        var parse = Parse(ListCreate(), "--name", "Ops", "--type", "email");

        var ex = Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildCreateNotificationListCommand(parse));
        Assert.Contains("--emails", ex.Message);
    }

    [Fact]
    public void ListCreate_SmsTypeWithEmails_Throws()
    {
        var parse = Parse(ListCreate(), 
            "--name", "Ops SMS", "--type", "sms", "--emails", "ops@example.com");

        var ex = Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildCreateNotificationListCommand(parse));
        Assert.Contains("--emails", ex.Message);
    }

    [Fact]
    public void ListCreate_UnknownType_Throws()
    {
        var parse = Parse(ListCreate(), 
            "--name", "Ops", "--type", "carrier-pigeon", "--emails", "ops@example.com");

        Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildCreateNotificationListCommand(parse));
    }

    [Fact]
    public void ListUpdate_SmsTypeWithPhones_BuildsSmsCommandWithRecipientPhones()
    {
        var parse = Parse(ListUpdate(), 
            "--id", "7", "--name", "Ops SMS", "--type", "sms", "--phones", "+15551230000");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildUpdateNotificationListCommand(parse);

        Assert.Equal(7, cmd.NotificationListId);
        Assert.Equal(NotificationType.Sms, cmd.Type);
        Assert.Equal(new[] { "+15551230000" }, cmd.RecipientPhones);
        Assert.Empty(cmd.RecipientEmails);
    }

    [Fact]
    public void ListUpdate_EmailTypeWithPhones_Throws()
    {
        var parse = Parse(ListUpdate(), 
            "--id", "7", "--name", "Ops", "--type", "email",
            "--emails", "ops@example.com", "--phones", "+15551230000");

        var ex = Assert.Throws<ArgumentException>(
            () => NotificationCommands.BuildUpdateNotificationListCommand(parse));
        Assert.Contains("--phones", ex.Message);
    }

    // ---- notification sms group -------------------------------------------

    [Fact]
    public void Notification_HasSmsGroupWithListAndUpdate()
    {
        var subNames = SmsGroup().Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("list", subNames);
        Assert.Contains("update", subNames);
    }

    [Fact]
    public void SmsUpdate_WithAllFlags_ProducesCommandCarryingThem()
    {
        var parse = Parse(SmsUpdate(), 
            "--id", "1", "--account-sid", "ACnew", "--from-number", "+15551110000",
            "--messaging-service-sid", "MGnew", "--api-base-url", "https://new.example.com",
            "--auth-token", "new-secret");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);

        Assert.Equal(1, cmd.SmsConfigId);
        Assert.Equal("ACnew", cmd.AccountSid);
        Assert.Equal("+15551110000", cmd.FromNumber);
        Assert.Equal("MGnew", cmd.MessagingServiceSid);
        Assert.Equal("https://new.example.com", cmd.ApiBaseUrl);
        Assert.Equal("new-secret", cmd.AuthToken);
    }

    [Fact]
    public void SmsUpdate_WithoutOptionalFlags_ProducesCommandWithNulls()
    {
        var parse = Parse(SmsUpdate(), 
            "--id", "2", "--account-sid", "AConly", "--from-number", "+15552220000");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);

        Assert.Equal(2, cmd.SmsConfigId);
        // messaging-service-sid maps to null (handler CLEARS it); auth-token / api-base-url
        // map to null (handler PRESERVES them) — all three optional here.
        Assert.Null(cmd.MessagingServiceSid);
        Assert.Null(cmd.ApiBaseUrl);
        Assert.Null(cmd.AuthToken);
    }

    [Fact]
    public void SmsUpdate_OptionalFlags_AreNotRequired()
    {
        var update = SmsUpdate();
        Assert.False(update.Options.Single(o => o.Name == "--from-number").Required);
        Assert.False(update.Options.Single(o => o.Name == "--messaging-service-sid").Required);
        Assert.False(update.Options.Single(o => o.Name == "--api-base-url").Required);
        Assert.False(update.Options.Single(o => o.Name == "--auth-token").Required);
    }

    [Fact]
    public void SmsUpdate_MessagingServiceSidOnly_NoFromNumber_Builds()
    {
        // Twilio Messaging-Service-only config: From number omitted, Messaging Service SID
        // supplied. The either-or validation accepts it and FromNumber maps to null.
        var parse = Parse(SmsUpdate(),
            "--id", "3", "--account-sid", "ACmsg", "--messaging-service-sid", "MGonly");

        Assert.Empty(parse.Errors);
        var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);

        Assert.Null(cmd.FromNumber);
        Assert.Equal("MGonly", cmd.MessagingServiceSid);
    }

    [Fact]
    public void SmsUpdate_NeitherFromNorMessagingService_Throws()
    {
        // Neither sender identity supplied => the build rejects it so an SMS config is
        // never left with no way to send (mirrors the UI + delivery-adapter validation).
        var parse = Parse(SmsUpdate(), "--id", "4", "--account-sid", "ACnone");

        Assert.Throws<ArgumentException>(() => NotificationCommands.BuildUpdateSmsConfigCommand(parse));
    }

    [Fact]
    public void SmsCommands_ResolveViaRegistry()
    {
        // The CLI calls GetCommandName for every command it sends; both SMS commands
        // must round-trip through the management command registry.
        Assert.Equal(typeof(ListSmsConfigsCommand),
            ManagementCommandRegistry.Resolve(ManagementCommandRegistry.GetCommandName(typeof(ListSmsConfigsCommand))));
        Assert.Equal(typeof(UpdateSmsConfigCommand),
            ManagementCommandRegistry.Resolve(ManagementCommandRegistry.GetCommandName(typeof(UpdateSmsConfigCommand))));
    }

    // ---- Auth Token is never rendered -------------------------------------
}

/// <summary>
/// Console-capturing companion to <see cref="NotificationSmsCommandTests"/> — pins that the
/// Twilio Auth Token is never echoed to the rendered output. Lives in the shared "Console"
/// collection so the <see cref="Console.SetOut"/> redirection does not race the other
/// console-capturing test classes.
/// </summary>
[Collection("Console")]
public class NotificationSmsAuthTokenRenderingTests
{
    [Fact]
    public void SmsConfigListResponse_RenderedAsJson_DoesNotContainAuthToken()
    {
        // The server projects the AuthToken away to a HasAuthToken presence flag
        // (SmsConfigPublicShape). The CLI renders that projected response verbatim,
        // so a rendered SMS-config list never surfaces the secret token value.
        var projected = new[]
        {
            new
            {
                Id = 1,
                AccountSid = "ACxxxx",
                FromNumber = "+15550000000",
                HasAuthToken = true,
            },
        };

        var original = Console.Out;
        using var sw = new StringWriter();
        try
        {
            Console.SetOut(sw);
            OutputFormatter.WriteJson(projected);
        }
        finally
        {
            Console.SetOut(original);
        }

        var rendered = sw.ToString();
        Assert.Contains("hasAuthToken", rendered);
        Assert.DoesNotContain("authToken\"", rendered);
        Assert.DoesNotContain("secret", rendered, StringComparison.OrdinalIgnoreCase);
    }
}
