using ZB.MOM.WW.MxGateway.Server.Security.Authentication;

namespace ZB.MOM.WW.MxGateway.Tests.Security.Authentication;

public sealed class ApiKeyAdminCommandLineParserTests
{
    /// <summary>
    /// Verifies non-API key commands return not-an-API-key result.
    /// </summary>
    [Fact]
    public void Parse_NonApiKeyCommand_ReturnsNotApiKeyCommand()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(["--urls=http://localhost:5000"]);

        Assert.False(result.IsApiKeyCommand);
        Assert.Null(result.Command);
    }

    /// <summary>
    /// Verifies API key create command parsing returns options.
    /// </summary>
    [Fact]
    public void Parse_CreateKeyCommand_ReturnsOptions()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            [
                "apikey",
                "create-key",
                "--key-id",
                "operator01",
                "--display-name",
                "Operator",
                "--scopes",
                "session:open,events:read",
                "--sqlite-path",
                "auth.db",
                "--pepper",
                "pepper",
                "--json"
            ]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Error);
        Assert.NotNull(result.Command);
        Assert.Equal(ApiKeyAdminCommandKind.CreateKey, result.Command.Kind);
        Assert.True(result.Command.Json);
        Assert.Equal("operator01", result.Command.KeyId);
        Assert.Equal("Operator", result.Command.DisplayName);
        Assert.Equal("auth.db", result.Command.SqlitePath);
        Assert.Equal("pepper", result.Command.Pepper);
        Assert.Contains("session:open", result.Command.Scopes);
        Assert.Contains("events:read", result.Command.Scopes);
    }

    /// <summary>A create-key command without --expires leaves the key non-expiring (opt-in).</summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithoutExpires_HasNoExpiry()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open"]);

        Assert.NotNull(result.Command);
        Assert.Null(result.Command.ExpiresUtc);
    }

    /// <summary>An absolute ISO-8601 --expires value is parsed to that exact UTC instant.</summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithAbsoluteExpires_ParsesInstant()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open",
             "--expires", "2030-01-02T03:04:05Z"]);

        Assert.NotNull(result.Command);
        Assert.Equal(
            new DateTimeOffset(2030, 1, 2, 3, 4, 5, TimeSpan.Zero),
            result.Command.ExpiresUtc);
    }

    /// <summary>A relative "&lt;N&gt;d" --expires value resolves to a future UTC instant.</summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithRelativeExpires_ResolvesToFuture()
    {
        DateTimeOffset before = DateTimeOffset.UtcNow;
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open",
             "--expires", "30d"]);

        Assert.NotNull(result.Command);
        Assert.NotNull(result.Command.ExpiresUtc);
        Assert.InRange(
            result.Command.ExpiresUtc!.Value,
            before + TimeSpan.FromDays(30) - TimeSpan.FromMinutes(1),
            DateTimeOffset.UtcNow + TimeSpan.FromDays(30) + TimeSpan.FromMinutes(1));
    }

    /// <summary>An unparseable --expires value fails at parse time.</summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithInvalidExpires_Fails()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator", "--scopes", "session:open",
             "--expires", "not-a-date"]);

        Assert.Null(result.Command);
        Assert.NotNull(result.Error);
        Assert.Contains("--expires", result.Error, StringComparison.Ordinal);
    }

    /// <summary>
    ///     A create-key command with a non-canonical scope
    ///     string (e.g. CLAUDE.md's stale <c>invoke</c> instead of <c>invoke:read</c>)
    ///     must be rejected at parse time rather than silently persisting an
    ///     unusable scope the authorization resolver never matches.
    /// </summary>
    [Fact]
    public void Parse_CreateKeyCommand_RejectsUnknownScope()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            [
                "apikey",
                "create-key",
                "--key-id",
                "operator01",
                "--display-name",
                "Operator",
                "--scopes",
                "session:open,invoke,metadata",
            ]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Command);
        Assert.NotNull(result.Error);
        Assert.Contains("invoke", result.Error, StringComparison.Ordinal);
        Assert.Contains("metadata", result.Error, StringComparison.Ordinal);
    }

    /// <summary>Verifies a create-key command with only canonical scopes parses successfully.</summary>
    [Fact]
    public void Parse_CreateKeyCommand_AcceptsAllCanonicalScopes()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            [
                "apikey",
                "create-key",
                "--key-id",
                "operator01",
                "--display-name",
                "Operator",
                "--scopes",
                "session:open,session:close,invoke:read,invoke:write,invoke:secure,events:read,metadata:read,admin",
            ]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Error);
        Assert.NotNull(result.Command);
        Assert.Equal(8, result.Command.Scopes.Count);
    }

    /// <summary>
    /// Verifies create key without display name returns error.
    /// </summary>
    [Fact]
    public void Parse_CreateKeyCommand_ReturnsConstraints()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            [
                "apikey",
                "create-key",
                "--key-id",
                "operator01",
                "--display-name",
                "Operator",
                "--read-subtree",
                "Area1/*",
                "--read-subtree",
                "Area2/*",
                "--write-tag-glob",
                "Pump_*",
                "--max-write-classification",
                "2",
                "--browse-subtree",
                "Area1/*",
                "--read-alarm-only",
                "--read-historized-only"
            ]);

        Assert.True(result.IsApiKeyCommand);
        Assert.NotNull(result.Command);
        ApiKeyConstraints constraints = result.Command.Constraints;
        Assert.Equal(["Area1/*", "Area2/*"], constraints.ReadSubtrees);
        Assert.Equal(["Pump_*"], constraints.WriteTagGlobs);
        Assert.Equal(2, constraints.MaxWriteClassification);
        Assert.Equal(["Area1/*"], constraints.BrowseSubtrees);
        Assert.True(constraints.ReadAlarmOnly);
        Assert.True(constraints.ReadHistorizedOnly);
    }

    /// <summary>
    ///     Verifies --dashboard-tags parses a comma-separated list, trimming segments and unioning
    ///     repeated occurrences of the flag without duplicating a tag that differs only by case.
    /// </summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithDashboardTags_ParsesTrimmedTagList()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            [
                "apikey",
                "create-key",
                "--key-id",
                "operator01",
                "--display-name",
                "Operator",
                "--dashboard-tags",
                " team-a , team-b ",
                "--dashboard-tags",
                "TEAM-A,team-c"
            ]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Error);
        Assert.NotNull(result.Command);
        Assert.Equal(["team-a", "team-b", "team-c"], result.Command.Constraints.DashboardTags);
    }

    /// <summary>Verifies a create-key command without --dashboard-tags leaves the key untagged.</summary>
    [Fact]
    public void Parse_CreateKeyCommand_WithoutDashboardTags_LeavesKeyUntagged()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator"]);

        Assert.NotNull(result.Command);
        Assert.Empty(result.Command.Constraints.DashboardTags);
    }

    /// <summary>
    ///     Verifies an empty tag segment is rejected rather than dropped: a stray comma must not
    ///     silently persist a grant the operator did not write.
    /// </summary>
    [Theory]
    [InlineData("team-a,,team-b")]
    [InlineData("team-a,  ")]
    [InlineData("")]
    public void Parse_CreateKeyCommand_WithEmptyDashboardTag_Fails(string tags)
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01", "--display-name", "Operator",
             $"--dashboard-tags={tags}"]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Command);
        Assert.NotNull(result.Error);
        Assert.Contains("--dashboard-tags", result.Error, StringComparison.Ordinal);
    }

    /// <summary>Verifies that create-key command without display name returns error.</summary>
    [Fact]
    public void Parse_CreateKeyWithoutDisplayName_ReturnsError()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "create-key", "--key-id", "operator01"]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Command);
        Assert.Contains("--display-name", result.Error, StringComparison.Ordinal);
    }

    /// <summary>
    /// Verifies key ID with underscore returns error.
    /// </summary>
    [Fact]
    public void Parse_KeyIdWithUnderscore_ReturnsError()
    {
        ApiKeyAdminParseResult result = ApiKeyAdminCommandLineParser.Parse(
            ["apikey", "revoke-key", "--key-id", "operator_01"]);

        Assert.True(result.IsApiKeyCommand);
        Assert.Null(result.Command);
        Assert.Contains("letters, numbers, periods, and hyphens", result.Error, StringComparison.Ordinal);
    }
}
