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),
        CachedCallCommands.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 (17):
        //   template, instance, site, deploy, data-connection, external-system,
        //   notification, security, audit-config, audit, health, debug,
        //   shared-script, db-connection, api-method, bundle, cached-call.
        Assert.Equal(17, 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 Deploy_HasSiteVerb_WithRequiredSiteId()
    {
        // WP2.5: bulk site deploy. Named `deploy site` to sit alongside
        // `deploy instance` / `deploy artifacts` — the group's convention is that
        // the verb names the SCOPE of the deploy.
        var deploy = DeployCommands.Build(Url, Format, Username, Password);
        var site = deploy.Subcommands.Single(c => c.Name == "site");

        // --site-id is REQUIRED here, unlike `deploy artifacts` where omitting it
        // means fleet-wide. A bulk instance deploy must never be accidentally
        // fleet-wide.
        Assert.NotEmpty(site.Parse([]).Errors);
        Assert.Empty(site.Parse(["--site-id", "3"]).Errors);
    }

    [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);
    }

    [Fact]
    public void TemplateFolder_HasFullManagementCommandParity()
    {
        // CLI/management parity follow-up (docs/plans/2026-05-11-templates-folder-hierarchy.md):
        // the folder management commands shipped without any CLI surface. Pin the
        // 1:1 verb set so a verb cannot silently disappear again.
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var subNames = folder.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("list", subNames);
        Assert.Contains("create", subNames);
        Assert.Contains("rename", subNames);
        Assert.Contains("move", subNames);
        Assert.Contains("reorder", subNames);
        Assert.Contains("delete", subNames);
    }

    [Fact]
    public void TemplateFolderCreate_ParentIdIsOptional_SoRootIsReachable()
    {
        // Omitting --parent-id must parse cleanly — that is how a folder is created
        // at the tree root (null ParentFolderId on CreateTemplateFolderCommand).
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var create = folder.Subcommands.Single(c => c.Name == "create");
        var parentOption = create.Options.Single(o => o.Name == "--parent-id");

        var rootParse = create.Parse(new[] { "--name", "Dev" });
        Assert.Empty(rootParse.Errors);
        Assert.Null(rootParse.GetValue((Option<int?>)parentOption));

        var nestedParse = create.Parse(new[] { "--name", "Sub", "--parent-id", "7" });
        Assert.Empty(nestedParse.Errors);
        Assert.Equal(7, nestedParse.GetValue((Option<int?>)parentOption));
    }

    [Fact]
    public void TemplateFolderMove_ParentIdIsOptional_SoRootIsReachable()
    {
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var move = folder.Subcommands.Single(c => c.Name == "move");
        var parentOption = move.Options.Single(o => o.Name == "--parent-id");

        var parse = move.Parse(new[] { "--id", "3" });
        Assert.Empty(parse.Errors);
        Assert.Null(parse.GetValue((Option<int?>)parentOption));
    }

    [Theory]
    [InlineData("up")]
    [InlineData("down")]
    public void TemplateFolderReorder_AcceptsUpAndDown(string direction)
    {
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var reorder = folder.Subcommands.Single(c => c.Name == "reorder");

        var parse = reorder.Parse(new[] { "--id", "3", "--direction", direction });
        Assert.Empty(parse.Errors);
    }

    [Theory]
    [InlineData("sideways")]
    // AcceptOnlyFromAmong is case-SENSITIVE (same contract as the audit
    // --channel / --kind / --status options), so the literals are lowercase only.
    [InlineData("Up")]
    public void TemplateFolderReorder_RejectsAnythingElse(string direction)
    {
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var reorder = folder.Subcommands.Single(c => c.Name == "reorder");

        var parse = reorder.Parse(new[] { "--id", "3", "--direction", direction });
        Assert.NotEmpty(parse.Errors);
    }

    [Fact]
    public void TemplateFolderReorder_RequiresDirection()
    {
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var folder = template.Subcommands.Single(c => c.Name == "folder");
        var reorder = folder.Subcommands.Single(c => c.Name == "reorder");

        var parse = reorder.Parse(new[] { "--id", "3" });
        Assert.NotEmpty(parse.Errors);
    }

    [Fact]
    public void SiteArea_HasMoveVerb()
    {
        // CLI/management parity follow-up (docs/plans/2026-05-11-deployment-topology-page-design.md):
        // AreaService.MoveAreaAsync had no management command and no CLI verb.
        var site = SiteCommands.Build(Url, Format, Username, Password);
        var area = site.Subcommands.Single(c => c.Name == "area");
        var subNames = area.Subcommands.Select(c => c.Name).ToHashSet();
        Assert.Contains("move", subNames);
    }

    [Fact]
    public void SiteAreaMove_ParentIdIsOptional_SoSiteRootIsReachable()
    {
        // Omitting --parent-id is how an area is moved to the site root
        // (null NewParentAreaId on MoveAreaCommand).
        var site = SiteCommands.Build(Url, Format, Username, Password);
        var area = site.Subcommands.Single(c => c.Name == "area");
        var move = area.Subcommands.Single(c => c.Name == "move");
        var parentOption = move.Options.Single(o => o.Name == "--parent-id");

        var rootParse = move.Parse(new[] { "--id", "3" });
        Assert.Empty(rootParse.Errors);
        Assert.Null(rootParse.GetValue((Option<int?>)parentOption));

        var reparentParse = move.Parse(new[] { "--id", "3", "--parent-id", "2" });
        Assert.Empty(reparentParse.Errors);
        Assert.Equal(2, reparentParse.GetValue((Option<int?>)parentOption));
    }

    [Fact]
    public void SiteAreaMove_RequiresId()
    {
        var site = SiteCommands.Build(Url, Format, Username, Password);
        var area = site.Subcommands.Single(c => c.Name == "area");
        var move = area.Subcommands.Single(c => c.Name == "move");

        var parse = move.Parse(Array.Empty<string>());
        Assert.NotEmpty(parse.Errors);
    }

    [Fact]
    public void TemplateList_HasSkipAndTakePagingOptions()
    {
        // arch-review P2: `template list` gained additive --skip/--take offset paging.
        var template = TemplateCommands.Build(Url, Format, Username, Password);
        var list = template.Subcommands.Single(c => c.Name == "list");
        var optionNames = list.Options.Select(o => o.Name).ToList();
        Assert.Contains("--skip", optionNames);
        Assert.Contains("--take", optionNames);
    }

    [Fact]
    public void InstanceList_HasSkipAndTakePagingOptions()
    {
        // arch-review P2: `instance list` gained additive --skip/--take offset paging.
        var instance = InstanceCommands.Build(Url, Format, Username, Password);
        var list = instance.Subcommands.Single(c => c.Name == "list");
        var optionNames = list.Options.Select(o => o.Name).ToList();
        Assert.Contains("--skip", optionNames);
        Assert.Contains("--take", optionNames);
    }

    [Fact]
    public void InstanceList_ParsesTakeOptionValue()
    {
        // Parse-level check that --take binds to an int value on the list command.
        var instance = InstanceCommands.Build(Url, Format, Username, Password);
        var list = instance.Subcommands.Single(c => c.Name == "list");
        var takeOption = list.Options.Single(o => o.Name == "--take");

        var parse = list.Parse(new[] { "--skip", "10", "--take", "5" });
        Assert.Empty(parse.Errors);
        Assert.Equal(5, parse.GetValue((Option<int?>)takeOption));
    }

    [Theory]
    [InlineData(typeof(GetInstanceCommand))]
    [InlineData(typeof(ListSitesCommand))]
    [InlineData(typeof(CreateTemplateCommand))]
    [InlineData(typeof(SetConnectionBindingsCommand))]
    [InlineData(typeof(SetInstanceOverridesCommand))]
    [InlineData(typeof(DebugSnapshotCommand))]
    [InlineData(typeof(MgmtDeployInstanceCommand))]
    [InlineData(typeof(MgmtDeploySiteCommand))]
    [InlineData(typeof(QueryAuditLogCommand))]
    [InlineData(typeof(ExportBundleCommand))]
    [InlineData(typeof(PreviewBundleCommand))]
    [InlineData(typeof(ImportBundleCommand))]
    [InlineData(typeof(AddTemplateNativeAlarmSourceCommand))]
    [InlineData(typeof(SetInstanceNativeAlarmSourceOverrideCommand))]
    [InlineData(typeof(MoveAreaCommand))]
    [InlineData(typeof(ListTemplateFoldersCommand))]
    [InlineData(typeof(CreateTemplateFolderCommand))]
    [InlineData(typeof(RenameTemplateFolderCommand))]
    [InlineData(typeof(MoveTemplateFolderCommand))]
    [InlineData(typeof(ReorderTemplateFolderCommand))]
    [InlineData(typeof(DeleteTemplateFolderCommand))]
    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));
    }
}
