using System.CommandLine;
using System.Net;
using System.Text;
using System.Text.Json;
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 tree</c> subcommand (Audit Log #23 M5.1-T8):
/// tree rendering (table format), JSON output, error handling, and CLI parsing.
/// </summary>
[Collection("Console")]
public class AuditTreeCommandTests
{
    // ─────────────────────────────────────────────────────────────────────
    // JSON parsing helpers
    // ─────────────────────────────────────────────────────────────────────

    private static string NodeJson(
        string executionId,
        string? parentId = null,
        int rowCount = 3,
        string[]? channels = null,
        string[]? statuses = null,
        string? siteId = "plant-a",
        string? instanceId = "inst-1",
        string? first = "2026-05-20T10:00:00Z",
        string? last = "2026-05-20T10:01:00Z")
    {
        var parentStr = parentId != null ? $"\"{parentId}\"" : "null";
        var channelArr = channels is { Length: > 0 }
            ? "[" + string.Join(",", channels.Select(c => $"\"{c}\"")) + "]"
            : "[\"ApiOutbound\"]";
        var statusArr = statuses is { Length: > 0 }
            ? "[" + string.Join(",", statuses.Select(s => $"\"{s}\"")) + "]"
            : "[\"Delivered\"]";
        var siteStr = siteId != null ? $"\"{siteId}\"" : "null";
        var instanceStr = instanceId != null ? $"\"{instanceId}\"" : "null";
        var firstStr = first != null ? $"\"{first}\"" : "null";
        var lastStr = last != null ? $"\"{last}\"" : "null";

        return $@"{{
""executionId"":""{executionId}"",
""parentExecutionId"":{parentStr},
""rowCount"":{rowCount},
""channels"":{channelArr},
""statuses"":{statusArr},
""sourceSiteId"":{siteStr},
""sourceInstanceId"":{instanceStr},
""firstOccurredAtUtc"":{firstStr},
""lastOccurredAtUtc"":{lastStr}
}}";
    }

    // ─────────────────────────────────────────────────────────────────────
    // ParseNodes
    // ─────────────────────────────────────────────────────────────────────

    [Fact]
    public void ParseNodes_ValidArray_ReturnsDtos()
    {
        var root = "11111111-1111-1111-1111-111111111111";
        var child = "22222222-2222-2222-2222-222222222222";
        var json = $"[{NodeJson(root)},{NodeJson(child, parentId: root)}]";

        var nodes = AuditTreeHelpers.ParseNodes(json);

        Assert.Equal(2, nodes.Length);
        Assert.Equal(Guid.Parse(root), nodes[0].ExecutionId);
        Assert.Null(nodes[0].ParentExecutionId);
        Assert.Equal(Guid.Parse(child), nodes[1].ExecutionId);
        Assert.Equal(Guid.Parse(root), nodes[1].ParentExecutionId);
        Assert.Equal(3, nodes[0].RowCount);
    }

    [Fact]
    public void ParseNodes_EmptyArray_ReturnsEmpty()
    {
        var nodes = AuditTreeHelpers.ParseNodes("[]");
        Assert.Empty(nodes);
    }

    [Fact]
    public void ParseNodes_InvalidJson_ReturnsEmpty()
    {
        var nodes = AuditTreeHelpers.ParseNodes("not-json");
        Assert.Empty(nodes);
    }

    // ─────────────────────────────────────────────────────────────────────
    // WriteTable — ASCII tree rendering
    // ─────────────────────────────────────────────────────────────────────

    [Fact]
    public void WriteTable_EmptyNodes_PrintsFallbackMessage()
    {
        var output = new StringWriter();
        AuditTreeHelpers.WriteTable(Array.Empty<AuditTreeNodeDto>(), Guid.NewGuid(), output);
        Assert.Contains("no execution tree found", output.ToString());
    }

    [Fact]
    public void WriteTable_SingleRootNode_PrintsWithNoIndent()
    {
        var rootId = Guid.Parse("11111111-1111-1111-1111-111111111111");
        var nodes = AuditTreeHelpers.ParseNodes($"[{NodeJson(rootId.ToString())}]");

        var output = new StringWriter();
        AuditTreeHelpers.WriteTable(nodes, rootId, output);
        var text = output.ToString();

        // Root node printed at column 0 (no leading spaces).
        var line = text.Split('\n', StringSplitOptions.RemoveEmptyEntries).First();
        Assert.StartsWith(rootId.ToString("D"), line);
        Assert.Contains("[*]", line);  // queried node marked
    }

    [Fact]
    public void WriteTable_MultiLevelTree_IndentsChildrenCorrectly()
    {
        var rootId = "11111111-1111-1111-1111-111111111111";
        var childId = "22222222-2222-2222-2222-222222222222";
        var grandChildId = "33333333-3333-3333-3333-333333333333";
        var json = $"[{NodeJson(rootId)},{NodeJson(childId, parentId: rootId)},{NodeJson(grandChildId, parentId: childId)}]";
        var nodes = AuditTreeHelpers.ParseNodes(json);

        var output = new StringWriter();
        AuditTreeHelpers.WriteTable(nodes, Guid.Parse(rootId), output);
        var lines = output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);

        // Root: no indent.
        Assert.True(lines[0].StartsWith(rootId, StringComparison.OrdinalIgnoreCase) ||
                    lines[0].StartsWith(rootId.ToUpper(), StringComparison.OrdinalIgnoreCase));

        // Child: 2-space indent (exactly 2, not 4+).
        var childLine = lines.First(l => l.Contains(childId));
        Assert.StartsWith("  ", childLine);
        Assert.False(childLine.StartsWith("    ", StringComparison.Ordinal), "child should be indented exactly 2, not 4+");

        // Grandchild: 4-space indent.
        var grandLine = lines.First(l => l.Contains(grandChildId));
        Assert.StartsWith("    ", grandLine);
    }

    [Fact]
    public void WriteTable_QueriedNodeIsMarked_OthersAreNot()
    {
        var rootId = Guid.Parse("11111111-1111-1111-1111-111111111111");
        var childId = Guid.Parse("22222222-2222-2222-2222-222222222222");
        var json = $"[{NodeJson(rootId.ToString())},{NodeJson(childId.ToString(), parentId: rootId.ToString())}]";
        var nodes = AuditTreeHelpers.ParseNodes(json);

        // Query via child ID — child should be marked, root should not.
        var output = new StringWriter();
        AuditTreeHelpers.WriteTable(nodes, childId, output);
        var lines = output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);

        var childLine = lines.First(l => l.Contains(childId.ToString("D")));
        var rootLine = lines.First(l => l.Contains(rootId.ToString("D")));
        Assert.Contains("[*]", childLine);
        Assert.DoesNotContain("[*]", rootLine);
    }

    // ─────────────────────────────────────────────────────────────────────
    // WriteJson
    // ─────────────────────────────────────────────────────────────────────

    [Fact]
    public void WriteJson_ValidNodes_EmitsValidJsonArray()
    {
        var rootId = "11111111-1111-1111-1111-111111111111";
        var childId = "22222222-2222-2222-2222-222222222222";
        var nodes = AuditTreeHelpers.ParseNodes($"[{NodeJson(rootId)},{NodeJson(childId, parentId: rootId)}]");

        var output = new StringWriter();
        AuditTreeHelpers.WriteJson(nodes, output);
        var text = output.ToString();

        using var doc = JsonDocument.Parse(text);
        Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
        Assert.Equal(2, doc.RootElement.GetArrayLength());
    }

    [Fact]
    public void WriteJson_EmptyNodes_EmitsEmptyArray()
    {
        var output = new StringWriter();
        AuditTreeHelpers.WriteJson(Array.Empty<AuditTreeNodeDto>(), output);
        var text = output.ToString().Trim();

        using var doc = JsonDocument.Parse(text);
        Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
        Assert.Equal(0, doc.RootElement.GetArrayLength());
    }

    // ─────────────────────────────────────────────────────────────────────
    // RunTreeAsync — HTTP execution
    // ─────────────────────────────────────────────────────────────────────

    private sealed class FixedHandler : HttpMessageHandler
    {
        private readonly HttpStatusCode _status;
        private readonly string _body;

        public FixedHandler(HttpStatusCode status, string body)
        {
            _status = status;
            _body = body;
        }

        public string? LastRequestUri { get; private set; }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            LastRequestUri = request.RequestUri!.PathAndQuery;
            return Task.FromResult(new HttpResponseMessage(_status)
            {
                Content = new StringContent(_body, Encoding.UTF8, "application/json"),
            });
        }
    }

    [Fact]
    public async Task RunTree_Success_ReturnsZeroAndWritesOutput()
    {
        var rootId = "11111111-1111-1111-1111-111111111111";
        var json = $"[{NodeJson(rootId)}]";
        var handler = new FixedHandler(HttpStatusCode.OK, json);
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditTreeHelpers.RunTreeAsync(
            client, Guid.Parse(rootId), "table", output);

        Assert.Equal(0, exit);
        Assert.Contains(rootId, output.ToString());
    }

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

        var exit = await AuditTreeHelpers.RunTreeAsync(
            client, Guid.NewGuid(), "table", output);

        Assert.Equal(0, exit);
        Assert.Contains("no execution tree found", output.ToString());
    }

    [Fact]
    public async Task RunTree_JsonFormat_EmitsValidJson()
    {
        var rootId = "11111111-1111-1111-1111-111111111111";
        var handler = new FixedHandler(HttpStatusCode.OK, $"[{NodeJson(rootId)}]");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        var exit = await AuditTreeHelpers.RunTreeAsync(
            client, Guid.Parse(rootId), "json", output);

        Assert.Equal(0, exit);
        using var doc = JsonDocument.Parse(output.ToString());
        Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
    }

    [Fact]
    public async Task RunTree_Http403_ReturnsExitCode2()
    {
        var handler = new FixedHandler(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 AuditTreeHelpers.RunTreeAsync(
            client, Guid.NewGuid(), "table", output);

        Assert.Equal(2, exit);
    }

    [Fact]
    public async Task RunTree_Http500_ReturnsExitCode1()
    {
        var handler = new FixedHandler(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 AuditTreeHelpers.RunTreeAsync(
            client, Guid.NewGuid(), "table", output);

        Assert.Equal(1, exit);
    }

    [Fact]
    public async Task RunTree_RequestUrlContainsExecutionId()
    {
        var id = Guid.Parse("11111111-1111-1111-1111-111111111111");
        var handler = new FixedHandler(HttpStatusCode.OK, "[]");
        var client = new ManagementHttpClient(new HttpClient(handler), "http://localhost:9001", "u", "p");
        var output = new StringWriter();

        await AuditTreeHelpers.RunTreeAsync(client, id, "table", output);

        Assert.Contains("11111111-1111-1111-1111-111111111111", handler.LastRequestUri);
        Assert.Contains("executionId", handler.LastRequestUri);
    }

    // ─────────────────────────────────────────────────────────────────────
    // CLI parsing — audit tree subcommand
    // ─────────────────────────────────────────────────────────────────────

    [Fact]
    public void Tree_Subcommand_ExistsInAuditCommandGroup()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var parse = root.Parse(new[] { "audit", "tree", "--help" });
        // --help is never an error, exit 0.
        Assert.Empty(parse.Errors);
    }

    [Fact]
    public void Tree_ExecutionIdOption_IsRequired()
    {
        // Invoking without --execution-id must produce an error (the option is Required).
        var root = AuditCommandTestHarness.BuildRoot();
        var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "tree");
        // System.CommandLine returns non-zero for a missing required option.
        Assert.NotEqual(0, exit);
    }

    [Fact]
    public void Tree_HelpText_DescribesExecutionId()
    {
        var root = AuditCommandTestHarness.BuildRoot();
        var output = new StringWriter();
        var exit = root.Parse(new[] { "audit", "tree", "--help" })
            .Invoke(new InvocationConfiguration { Output = output });

        Assert.Equal(0, exit);
        Assert.Contains("execution-id", output.ToString());
    }
}
