using System.Collections.Specialized;
using System.Net;
using System.Text;
using System.Web;
using ZB.MOM.WW.ScadaBridge.CLI;
using ZB.MOM.WW.ScadaBridge.CLI.Commands;

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

/// <summary>
/// Tests for the <c>scadabridge audit query</c> subcommand (Audit Log #23 M8-T2):
/// time-spec resolution, query-string construction, formatter selection, error
/// handling, and keyset-cursor paging via <c>--all</c>.
/// </summary>
[Collection("Console")]
public class AuditQueryCommandTests
{
    // ---- Time-spec parsing -------------------------------------------------

    [Fact]
    public void ResolveTimeSpec_RelativeHours_ResolvesToNowMinusOffset()
    {
        var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
        var resolved = AuditQueryHelpers.ResolveTimeSpec("1h", now);
        Assert.Equal(DateTimeOffset.Parse("2026-05-20T11:00:00Z"), resolved);
    }

    [Fact]
    public void ResolveTimeSpec_RelativeDays_ResolvesToNowMinusOffset()
    {
        var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
        var resolved = AuditQueryHelpers.ResolveTimeSpec("7d", now);
        Assert.Equal(DateTimeOffset.Parse("2026-05-13T12:00:00Z"), resolved);
    }

    [Fact]
    public void ResolveTimeSpec_AbsoluteIso8601_ParsedVerbatim()
    {
        var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
        var resolved = AuditQueryHelpers.ResolveTimeSpec("2026-01-02T03:04:05Z", now);
        Assert.Equal(DateTimeOffset.Parse("2026-01-02T03:04:05Z"), resolved);
    }

    [Fact]
    public void ResolveTimeSpec_Garbage_Throws()
    {
        var now = DateTimeOffset.UtcNow;
        Assert.Throws<FormatException>(() => AuditQueryHelpers.ResolveTimeSpec("not-a-time", now));
    }

    // ---- Query string construction ----------------------------------------

    [Fact]
    public void BuildQueryString_FullFlagSet_ProducesExpectedParameters()
    {
        var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
        var args = new AuditQueryArgs
        {
            Since = "1h",
            Until = "2026-05-20T12:00:00Z",
            Channel = new[] { "ApiOutbound" },
            Kind = new[] { "ApiCallCached" },
            Status = new[] { "Delivered" },
            Site = new[] { "site-1" },
            Target = "weather-api",
            Actor = "multi-role",
            CorrelationId = "abc-123",
            ExecutionId = "def-456",
            ParentExecutionId = "ghi-789",
            ErrorsOnly = false,
            PageSize = 250,
        };

        var qs = AuditQueryHelpers.BuildQueryString(args, now, afterOccurredAtUtc: null, afterEventId: null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));

        Assert.Equal("ApiOutbound", parsed["channel"]);
        Assert.Equal("ApiCallCached", parsed["kind"]);
        Assert.Equal("Delivered", parsed["status"]);
        Assert.Equal("site-1", parsed["sourceSiteId"]);
        // The CLI audit query has no --instance flag, so no instance param is emitted.
        Assert.Null(parsed["instance"]);
        Assert.Equal("weather-api", parsed["target"]);
        Assert.Equal("multi-role", parsed["actor"]);
        Assert.Equal("abc-123", parsed["correlationId"]);
        Assert.Equal("def-456", parsed["executionId"]);
        Assert.Equal("ghi-789", parsed["parentExecutionId"]);
        Assert.Equal("250", parsed["pageSize"]);
        Assert.Equal("2026-05-20T11:00:00.0000000+00:00", parsed["fromUtc"]);
        Assert.Equal("2026-05-20T12:00:00.0000000+00:00", parsed["toUtc"]);
    }

    [Fact]
    public void BuildQueryString_ErrorsOnly_MapsToFailedStatus()
    {
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs { ErrorsOnly = true };
        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
        Assert.Equal("Failed", parsed["status"]);
    }

    [Fact]
    public void BuildQueryString_MultiValueChannel_EmitsOneKeyPerValue()
    {
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs
        {
            Channel = new[] { "ApiOutbound", "DbOutbound" },
            Status = new[] { "Failed", "Parked" },
            Site = new[] { "site-1", "site-2" },
        };

        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));

        Assert.Equal(new[] { "ApiOutbound", "DbOutbound" }, parsed.GetValues("channel"));
        Assert.Equal(new[] { "Failed", "Parked" }, parsed.GetValues("status"));
        Assert.Equal(new[] { "site-1", "site-2" }, parsed.GetValues("sourceSiteId"));
    }

    [Fact]
    public void BuildQueryString_ErrorsOnly_OverridesExplicitStatusValues()
    {
        // --errors-only stays a single-status override: it pins status=Failed and
        // supersedes any explicit (multi-value) --status selection.
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs
        {
            ErrorsOnly = true,
            Status = new[] { "Delivered", "Parked" },
        };

        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));

        Assert.Equal(new[] { "Failed" }, parsed.GetValues("status"));
    }

    [Fact]
    public void BuildQueryString_Cursor_AppendsAfterParameters()
    {
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs();
        var after = DateTimeOffset.Parse("2026-05-20T10:00:00Z");
        var qs = AuditQueryHelpers.BuildQueryString(args, now, after, "evt-99");
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
        Assert.Equal("evt-99", parsed["afterEventId"]);
        Assert.Equal("2026-05-20T10:00:00.0000000+00:00", parsed["afterOccurredAtUtc"]);
    }

    [Fact]
    public void BuildQueryString_OmitsUnsetFilters()
    {
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs { PageSize = 100 };
        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
        Assert.Null(parsed["channel"]);
        Assert.Null(parsed["status"]);
        Assert.Null(parsed["fromUtc"]);
        Assert.Null(parsed["correlationId"]);
        Assert.Null(parsed["executionId"]);
        Assert.Null(parsed["parentExecutionId"]);
        Assert.Equal("100", parsed["pageSize"]);
    }

    [Fact]
    public void BuildQueryString_ExecutionId_EmitsExecutionIdParameter()
    {
        // --execution-id is a single-value Guid filter — mirrors --correlation-id.
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs { ExecutionId = "11111111-1111-1111-1111-111111111111" };
        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
        Assert.Equal("11111111-1111-1111-1111-111111111111", parsed["executionId"]);
    }

    [Fact]
    public void BuildQueryString_ParentExecutionId_EmitsParentExecutionIdParameter()
    {
        // --parent-execution-id is a single-value Guid filter — mirrors --execution-id.
        var now = DateTimeOffset.UtcNow;
        var args = new AuditQueryArgs { ParentExecutionId = "22222222-2222-2222-2222-222222222222" };
        var qs = AuditQueryHelpers.BuildQueryString(args, now, null, null);
        var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
        Assert.Equal("22222222-2222-2222-2222-222222222222", parsed["parentExecutionId"]);
    }

    // ---- HTTP execution / paging ------------------------------------------

    private sealed class RecordingHandler : HttpMessageHandler
    {
        private readonly Queue<string> _bodies;
        public List<string> RequestUris { get; } = new();

        public RecordingHandler(params string[] bodies)
        {
            _bodies = new Queue<string>(bodies);
        }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            RequestUris.Add(request.RequestUri!.PathAndQuery);
            var body = _bodies.Count > 0 ? _bodies.Dequeue() : "{\"events\":[],\"nextCursor\":null}";
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(body, Encoding.UTF8, "application/json"),
            });
        }
    }

    [Fact]
    public async Task RunQuery_SinglePage_WritesEventsAsJsonLines()
    {
        var handler = new RecordingHandler(
            "{\"events\":[{\"eventId\":\"e1\"},{\"eventId\":\"e2\"}],\"nextCursor\":null}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs { PageSize = 100 }, fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(0, exit);
        var lines = output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
        Assert.Equal(2, lines.Length);
        Assert.Contains("e1", lines[0]);
        Assert.Contains("e2", lines[1]);
        Assert.Single(handler.RequestUris);
    }

    [Fact]
    public async Task RunQuery_WithAll_FollowsNextCursorAcrossPages()
    {
        var handler = new RecordingHandler(
            "{\"events\":[{\"eventId\":\"e1\"}],\"nextCursor\":{\"afterOccurredAtUtc\":\"2026-05-20T10:00:00Z\",\"afterEventId\":\"e1\"}}",
            "{\"events\":[{\"eventId\":\"e2\"}],\"nextCursor\":null}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs { PageSize = 100 }, fetchAll: true,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(0, exit);
        Assert.Equal(2, handler.RequestUris.Count);
        Assert.Contains("afterEventId=e1", handler.RequestUris[1]);
        var lines = output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
        Assert.Equal(2, lines.Length);
    }

    [Fact]
    public async Task RunQuery_WithoutAll_StopsAfterFirstPageEvenWhenCursorPresent()
    {
        var handler = new RecordingHandler(
            "{\"events\":[{\"eventId\":\"e1\"}],\"nextCursor\":{\"afterOccurredAtUtc\":\"2026-05-20T10:00:00Z\",\"afterEventId\":\"e1\"}}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs { PageSize = 100 }, fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(0, exit);
        Assert.Single(handler.RequestUris);
    }

    [Fact]
    public async Task RunQuery_ServerError_ReturnsNonZeroExit()
    {
        var handler = new ErrorHandler();
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs(), fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.NotEqual(0, exit);
    }

    [Fact]
    public async Task RunQuery_Http403_ReturnsExitCode2()
    {
        // CLI-018: an authorization failure on /api/audit/query (HTTP 403) must
        // produce exit code 2 per the documented CLI exit-code contract — the
        // legacy bare-1 return masked auth failures as generic command failures.
        var handler = new StatusHandler(HttpStatusCode.Forbidden, "{\"error\":\"nope\",\"code\":\"UNAUTHORIZED\"}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs(), fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(2, exit);
    }

    [Fact]
    public async Task RunQuery_UnauthorizedCodeOnNon403_ReturnsExitCode2()
    {
        // The server may signal authorization failure via the error code on a
        // non-403 status (e.g. 400 + code=UNAUTHORIZED). Honour both channels.
        var handler = new StatusHandler(HttpStatusCode.BadRequest, "{\"error\":\"nope\",\"code\":\"UNAUTHORIZED\"}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs(), fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(2, exit);
    }

    [Fact]
    public async Task RunQuery_GenericServerError_ReturnsExitCode1()
    {
        // Authentication / internal errors (non-403, no auth code) must remain
        // exit code 1 — exit 2 is reserved for authorization failures.
        var handler = new StatusHandler(HttpStatusCode.InternalServerError, "{\"error\":\"boom\",\"code\":\"INTERNAL\"}");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditQueryHelpers.RunQueryAsync(
            client, new AuditQueryArgs(), fetchAll: false,
            new JsonLinesAuditFormatter(), output, DateTimeOffset.UtcNow);

        Assert.Equal(1, exit);
    }

    private sealed class ErrorHandler : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
            => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("{\"error\":\"boom\",\"code\":\"INTERNAL\"}"),
            });
    }

    private sealed class StatusHandler : HttpMessageHandler
    {
        private readonly HttpStatusCode _status;
        private readonly string _body;
        public StatusHandler(HttpStatusCode status, string body) { _status = status; _body = body; }
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
            => Task.FromResult(new HttpResponseMessage(_status) { Content = new StringContent(_body) });
    }

    // ---- CLI parsing -------------------------------------------------------

    [Fact]
    public void Query_UnknownFlag_ProducesParseErrorAndNonZeroExit()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "query", "--bogus", "x");
        Assert.NotEqual(0, exit);
        Assert.NotEqual("", err);
    }

    [Fact]
    public void Query_FormatTable_IsAccepted()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[] { "audit", "query", "--format", "table" });
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Query_ExecutionIdOption_IsAccepted()
    {
        // --execution-id is a single-value option — mirrors --correlation-id.
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[]
        {
            "audit", "query", "--execution-id", "11111111-1111-1111-1111-111111111111",
        });
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Query_ParentExecutionIdOption_IsAccepted()
    {
        // --parent-execution-id is a single-value option — mirrors --execution-id.
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[]
        {
            "audit", "query", "--parent-execution-id", "22222222-2222-2222-2222-222222222222",
        });
        Assert.Empty(parse.Errors);
    }

    // ---- Enum-name validation (fast-fail) ----------------------------------

    [Fact]
    public void Query_ChannelWithRealEnumName_IsAccepted()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[] { "audit", "query", "--channel", "ApiOutbound" });
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Query_MultipleChannelValues_SingleToken_AreAccepted()
    {
        // AllowMultipleArgumentsPerToken: --channel A B parses as two values.
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[] { "audit", "query", "--channel", "ApiOutbound", "DbOutbound" });
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Query_MultipleChannelValues_RepeatedFlag_AreAccepted()
    {
        // --channel A --channel B parses as two values.
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[]
        {
            "audit", "query", "--channel", "ApiOutbound", "--channel", "Notification",
        });
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Query_MultiValueChannel_WithOneInvalidName_FailsFast()
    {
        // AcceptOnlyFromAmong validates EACH value of the multi-value option.
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(
            root, "audit", "query", "--channel", "ApiOutbound", "OutboundApi");
        Assert.NotEqual(0, exit);
        Assert.NotEqual("", err);
    }

    [Fact]
    public void Query_ChannelWithInvalidName_FailsFast_NonZeroExit()
    {
        // "OutboundApi" is the old (non-existent) name; the real enum is "ApiOutbound".
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "query", "--channel", "OutboundApi");
        Assert.NotEqual(0, exit);
        Assert.NotEqual("", err);
        Assert.Contains("OutboundApi", err);
    }

    [Fact]
    public void Query_KindWithInvalidName_FailsFast_NonZeroExit()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "query", "--kind", "CachedCall");
        Assert.NotEqual(0, exit);
        Assert.NotEqual("", err);
    }

    [Fact]
    public void Query_StatusWithInvalidName_FailsFast_NonZeroExit()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "query", "--status", "Bogus");
        Assert.NotEqual(0, exit);
        Assert.NotEqual("", err);
    }
}
