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

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

/// <summary>
/// Regression tests for CLI-013 — the command-tree wiring was untested. These tests
/// build every command group and assert the tree is well-formed (every leaf has an
/// action, no group is empty), and that every management command record the CLI sends
/// resolves via <see cref="ManagementCommandRegistry"/> (so command-name derivation
/// never throws at runtime).
/// </summary>
public class CommandTreeTests
{
    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();

    // NOTE: this list MUST stay in sync with the rootCommand.Add(...) calls in
    // src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs. When a new command group is added (or one is
    // removed/renamed), update this array and bump the count assertion in
    // AllCommandGroups_Build_WithoutThrowing accordingly.
    private static IEnumerable<Command> AllCommandGroups() => new[]
    {
        TemplateCommands.Build(Url, Format, Username, Password),
        InstanceCommands.Build(Url, Format, Username, Password),
        SiteCommands.Build(Url, Format, Username, Password),
        DeployCommands.Build(Url, Format, Username, Password),
        DataConnectionCommands.Build(Url, Format, Username, Password),
        ExternalSystemCommands.Build(Url, Format, Username, Password),
        NotificationCommands.Build(Url, Format, Username, Password),
        SecurityCommands.Build(Url, Format, Username, Password),
        AuditLogCommands.Build(Url, Format, Username, Password),
        AuditCommands.Build(Url, Format, Username, Password),
        HealthCommands.Build(Url, Format, Username, Password),
        DebugCommands.Build(Url, Format, Username, Password),
        SharedScriptCommands.Build(Url, Format, Username, Password),
        DbConnectionCommands.Build(Url, Format, Username, Password),
        ApiMethodCommands.Build(Url, Format, Username, Password),
        BundleCommands.Build(Url, Format, Username, Password),
    };

    private static IEnumerable<Command> LeafCommands(Command command)
    {
        if (command.Subcommands.Count == 0)
        {
            yield return command;
            yield break;
        }

        foreach (var sub in command.Subcommands)
            foreach (var leaf in LeafCommands(sub))
                yield return leaf;
    }

    [Fact]
    public void AllCommandGroups_Build_WithoutThrowing()
    {
        var groups = AllCommandGroups().ToList();
        // CLI-022: bump this count whenever a new top-level command group is
        // registered in Program.cs. Current registered groups (16):
        //   template, instance, site, deploy, data-connection, external-system,
        //   notification, security, audit-config, audit, health, debug,
        //   shared-script, db-connection, api-method, bundle.
        Assert.Equal(16, groups.Count);
        Assert.All(groups, g => Assert.False(string.IsNullOrWhiteSpace(g.Name)));
    }

    [Fact]
    public void AllCommandGroups_Contains_AuditAndBundle()
    {
        // CLI-022: explicit group-presence assertion so the harness does not
        // silently drift back to excluding new groups. Use names because that
        // is what users actually type at the prompt.
        var groupNames = AllCommandGroups().Select(g => g.Name).ToHashSet();
        Assert.Contains("audit", groupNames);
        Assert.Contains("bundle", groupNames);
    }

    [Fact]
    public void AuditCommandGroup_HasQueryExportAndVerifyChain()
    {
        // CLI-022: pin the audit sub-command surface so a rename / accidental
        // removal of one of these is caught.
        var audit = AuditCommands.Build(Url, Format, Username, Password);
        var subNames = audit.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("query", subNames);
        Assert.Contains("export", subNames);
        Assert.Contains("verify-chain", subNames);
    }

    [Fact]
    public void BundleCommandGroup_HasExportPreviewAndImport()
    {
        // CLI-022: pin the bundle sub-command surface.
        var bundle = BundleCommands.Build(Url, Format, Username, Password);
        var subNames = bundle.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("export", subNames);
        Assert.Contains("preview", subNames);
        Assert.Contains("import", subNames);
    }

    [Fact]
    public void EveryLeafCommand_HasAnAction()
    {
        // A leaf command with no action is dead wiring — invoking it would do nothing.
        var leaves = AllCommandGroups().SelectMany(LeafCommands).ToList();

        Assert.NotEmpty(leaves);
        Assert.All(leaves, leaf =>
            Assert.True(leaf.Action != null, $"Leaf command '{leaf.Name}' has no action."));
    }

    [Fact]
    public void TemplateCompositionDelete_IsKeyedByIdOnly()
    {
        // CLI-015: the in-repo README documented `template composition delete` with
        // --template-id / --instance-name, but the implementation keys deletion by the
        // composition's own integer ID via a single --id option. Pin the real surface.
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var composition = template.Subcommands.Single(c => c.Name == "composition");
        var delete = composition.Subcommands.Single(c => c.Name == "delete");

        var optionNames = delete.Options.Select(o => o.Name).ToList();
        Assert.Contains("--id", optionNames);
        Assert.DoesNotContain("--template-id", optionNames);
        Assert.DoesNotContain("--instance-name", optionNames);
    }

    [Fact]
    public void TemplateNativeAlarmSource_HasAddListRemove()
    {
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var group = template.Subcommands.Single(c => c.Name == "native-alarm-source");
        var subNames = group.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("add", subNames);
        Assert.Contains("list", subNames);
        Assert.Contains("remove", subNames);
    }

    [Fact]
    public void InstanceNativeAlarmSource_HasSetAndClear()
    {
        var instance = InstanceCommands.Build(Url, Format, Username, Password);
        var group = instance.Subcommands.Single(c => c.Name == "native-alarm-source");
        var subNames = group.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("set", subNames);
        Assert.Contains("clear", subNames);
    }

    [Theory]
    [InlineData(typeof(GetInstanceCommand))]
    [InlineData(typeof(ListSitesCommand))]
    [InlineData(typeof(CreateTemplateCommand))]
    [InlineData(typeof(SetConnectionBindingsCommand))]
    [InlineData(typeof(SetInstanceOverridesCommand))]
    [InlineData(typeof(DebugSnapshotCommand))]
    [InlineData(typeof(MgmtDeployInstanceCommand))]
    [InlineData(typeof(QueryAuditLogCommand))]
    [InlineData(typeof(ExportBundleCommand))]
    [InlineData(typeof(PreviewBundleCommand))]
    [InlineData(typeof(ImportBundleCommand))]
    [InlineData(typeof(AddTemplateNativeAlarmSourceCommand))]
    [InlineData(typeof(SetInstanceNativeAlarmSourceOverrideCommand))]
    public void CommandPayloadTypes_ResolveViaRegistry(Type commandType)
    {
        // GetCommandName throws ArgumentException for an unregistered type — the CLI
        // calls it for every command it sends, so each must round-trip.
        var name = ManagementCommandRegistry.GetCommandName(commandType);
        Assert.False(string.IsNullOrWhiteSpace(name));
        Assert.Equal(commandType, ManagementCommandRegistry.Resolve(name));
    }
}
