using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;

/// <summary>
///     Unit tests for the plain-C# core of <see cref="MTConnectDriverForm"/> — the pure form model and
///     its <c>DriverConfig</c> JSON projection. There is no bUnit in this project, so every decision the
///     form makes lives here (in code a test can reach) rather than in razor markup.
/// </summary>
/// <remarks>
///     The JSON shape asserted below is the one <c>MTConnectDriver.ParseOptions</c> binds
///     (<c>agentUri</c> / <c>deviceName</c> / <c>requestTimeoutMs</c> / <c>sampleIntervalMs</c> /
///     <c>sampleCount</c> / <c>heartbeatMs</c> / <c>probe</c> / <c>reconnect</c>). It is deliberately
///     NOT <c>MTConnectDriverOptions</c>: that type carries <c>TimeSpan</c> probe knobs, which serialise
///     as <c>"00:00:05"</c> and would not bind to the driver's <c>intervalMs</c> integers.
/// </remarks>
public sealed class MTConnectDriverFormModelTests
{
    // Mirrors MTConnectDriverForm._jsonOpts for the read side of the assertions.
    private static readonly JsonSerializerOptions _opts = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        WriteIndented = false,
        Converters = { new JsonStringEnumConverter() },
    };

    private static JsonObject Emit(MTConnectDriverForm.MTConnectFormModel form, string? existingJson = null)
        => JsonNode.Parse(MTConnectDriverForm.BuildConfigJson(form, existingJson))!.AsObject();

    // ---- default shape -------------------------------------------------------------------------

    [Fact]
    public void Default_form_emits_a_config_the_driver_can_parse()
    {
        var o = Emit(new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000" });

        o["agentUri"]!.GetValue<string>().ShouldBe("http://agent:5000");
        o["requestTimeoutMs"]!.GetValue<int>().ShouldBe(5000);
        o["sampleIntervalMs"]!.GetValue<int>().ShouldBe(1000);
        o["sampleCount"]!.GetValue<int>().ShouldBe(1000);
        o["heartbeatMs"]!.GetValue<int>().ShouldBe(10000);
        o["probe"]!["enabled"]!.GetValue<bool>().ShouldBeTrue();
        o["probe"]!["intervalMs"]!.GetValue<int>().ShouldBe(5000);
        o["probe"]!["timeoutMs"]!.GetValue<int>().ShouldBe(2000);
        o["reconnect"]!["maxBackoffMs"]!.GetValue<int>().ShouldBe(30000);
        o["reconnect"]!["backoffMultiplier"]!.GetValue<double>().ShouldBe(2.0);
    }

    [Fact]
    public void Blank_agent_uri_is_omitted_rather_than_written_as_an_empty_string()
    {
        // An empty "agentUri" and an absent one both fail ParseOptions, but omitting it keeps the
        // driver's own "missing required AgentUri" message accurate.
        var o = Emit(new MTConnectDriverForm.MTConnectFormModel { AgentUri = "   " });
        o.ContainsKey("agentUri").ShouldBeFalse();
    }

    [Fact]
    public void Agent_uri_and_device_name_are_trimmed_and_blank_device_name_is_omitted()
    {
        var o = Emit(new MTConnectDriverForm.MTConnectFormModel
        {
            AgentUri = "  http://agent:5000  ",
            DeviceName = "   ",
        });

        o["agentUri"]!.GetValue<string>().ShouldBe("http://agent:5000");
        o.ContainsKey("deviceName").ShouldBeFalse();
    }

    // ---- arch-review 01/S-6: a zero must never reach the driver -------------------------------

    [Theory]
    [InlineData("requestTimeoutMs")]
    [InlineData("sampleIntervalMs")]
    [InlineData("sampleCount")]
    [InlineData("heartbeatMs")]
    public void Non_positive_timing_knobs_are_omitted_so_the_driver_applies_its_own_default(string key)
    {
        var form = new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000" };
        switch (key)
        {
            case "requestTimeoutMs": form.RequestTimeoutMs = 0; break;
            case "sampleIntervalMs": form.SampleIntervalMs = -1; break;
            case "sampleCount": form.SampleCount = 0; break;
            case "heartbeatMs": form.HeartbeatMs = 0; break;
        }

        // MTConnectDriver.StartCore calls RequirePositive on all four and faults the driver at
        // Initialize; omitting the key means ParseOptions substitutes the positive default instead.
        Emit(form).ContainsKey(key).ShouldBeFalse();
    }

    [Fact]
    public void Non_positive_probe_knobs_are_omitted_so_the_driver_applies_its_own_default()
    {
        var o = Emit(new MTConnectDriverForm.MTConnectFormModel
        {
            AgentUri = "http://agent:5000",
            ProbeIntervalMs = 0,
            ProbeTimeoutMs = -5,
        });

        var probe = o["probe"]!.AsObject();
        probe["enabled"]!.GetValue<bool>().ShouldBeTrue();
        probe.ContainsKey("intervalMs").ShouldBeFalse();
        probe.ContainsKey("timeoutMs").ShouldBeFalse();
    }

    [Fact]
    public void Zero_reconnect_min_backoff_is_preserved_because_the_driver_allows_it()
    {
        // MinBackoffMs = 0 means "retry immediately" and is the driver's own default — it must NOT be
        // swept up by the positive-only rule that guards the four RequirePositive knobs.
        var o = Emit(new MTConnectDriverForm.MTConnectFormModel
        {
            AgentUri = "http://agent:5000",
            ReconnectMinBackoffMs = 0,
        });

        o["reconnect"]!["minBackoffMs"]!.GetValue<int>().ShouldBe(0);
    }

    // ---- Validate ------------------------------------------------------------------------------

    [Fact]
    public void Validate_rejects_a_blank_agent_uri()
        => new MTConnectDriverForm.MTConnectFormModel { AgentUri = "" }
            .Validate().ShouldNotBeNullOrEmpty();

    [Fact]
    public void Validate_rejects_a_non_positive_timing_knob()
        => new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000", SampleCount = 0 }
            .Validate().ShouldNotBeNullOrEmpty();

    [Fact]
    public void Validate_ignores_probe_knobs_when_probing_is_off()
        => new MTConnectDriverForm.MTConnectFormModel
        {
            AgentUri = "http://agent:5000",
            ProbeEnabled = false,
            ProbeIntervalMs = 0,
            ProbeTimeoutMs = 0,
        }.Validate().ShouldBeNull();

    [Fact]
    public void Validate_accepts_a_fully_populated_form()
        => new MTConnectDriverForm.MTConnectFormModel { AgentUri = "http://agent:5000" }
            .Validate().ShouldBeNull();

    // ---- round trip ----------------------------------------------------------------------------

    [Fact]
    public void Round_trip_preserves_every_authored_value()
    {
        var original = new MTConnectDriverForm.MTConnectFormModel
        {
            AgentUri = "http://cell-a:5000",
            DeviceName = "VMC-1",
            RequestTimeoutMs = 7000,
            SampleIntervalMs = 250,
            SampleCount = 500,
            HeartbeatMs = 15000,
            ProbeEnabled = false,
            ProbeIntervalMs = 9000,
            ProbeTimeoutMs = 3000,
            ReconnectMinBackoffMs = 100,
            ReconnectMaxBackoffMs = 60000,
            ReconnectBackoffMultiplier = 1.5,
        };

        var json = MTConnectDriverForm.BuildConfigJson(original, null);
        var back = MTConnectDriverForm.MTConnectFormModel.FromJson(json);

        back.AgentUri.ShouldBe("http://cell-a:5000");
        back.DeviceName.ShouldBe("VMC-1");
        back.RequestTimeoutMs.ShouldBe(7000);
        back.SampleIntervalMs.ShouldBe(250);
        back.SampleCount.ShouldBe(500);
        back.HeartbeatMs.ShouldBe(15000);
        back.ProbeEnabled.ShouldBeFalse();
        back.ProbeIntervalMs.ShouldBe(9000);
        back.ProbeTimeoutMs.ShouldBe(3000);
        back.ReconnectMinBackoffMs.ShouldBe(100);
        back.ReconnectMaxBackoffMs.ShouldBe(60000);
        back.ReconnectBackoffMultiplier.ShouldBe(1.5);
    }

    [Fact]
    public void FromJson_reads_a_pascal_case_seeded_config()
    {
        // Seed SQL / hand-authored configs are PascalCase; the driver reads them case-insensitively,
        // so the form must too or it silently shows defaults over a real config.
        const string seeded = """
            {
              "AgentUri": "http://10.100.0.35:5000",
              "DeviceName": "Mill-01",
              "RequestTimeoutMs": 8000,
              "Probe": { "Enabled": false, "IntervalMs": 7000, "TimeoutMs": 4000 },
              "Reconnect": { "MinBackoffMs": 250, "MaxBackoffMs": 45000, "BackoffMultiplier": 3.0 }
            }
            """;

        var form = MTConnectDriverForm.MTConnectFormModel.FromJson(seeded);

        form.AgentUri.ShouldBe("http://10.100.0.35:5000");
        form.DeviceName.ShouldBe("Mill-01");
        form.RequestTimeoutMs.ShouldBe(8000);
        form.ProbeEnabled.ShouldBeFalse();
        form.ProbeIntervalMs.ShouldBe(7000);
        form.ProbeTimeoutMs.ShouldBe(4000);
        form.ReconnectMinBackoffMs.ShouldBe(250);
        form.ReconnectMaxBackoffMs.ShouldBe(45000);
        form.ReconnectBackoffMultiplier.ShouldBe(3.0);
    }

    [Fact]
    public void FromJson_falls_back_to_defaults_for_an_unusable_document()
    {
        foreach (var bad in new[] { null, "", "   ", "{", "\"scalar\"" })
        {
            var form = MTConnectDriverForm.MTConnectFormModel.FromJson(bad);
            form.RequestTimeoutMs.ShouldBe(5000);
            form.SampleCount.ShouldBe(1000);
        }
    }

    [Fact]
    public void Unknown_top_level_keys_survive_a_load_save()
    {
        // The driver also accepts a hand-authored tags[] surface; opening the form must not delete it.
        const string existing = """{"agentUri":"http://old:5000","tags":[{"fullName":"x1"}]}""";

        var form = MTConnectDriverForm.MTConnectFormModel.FromJson(existing);
        form.AgentUri = "http://new:5000";

        var o = Emit(form, existing);
        o["agentUri"]!.GetValue<string>().ShouldBe("http://new:5000");
        o["tags"]!.AsArray().Count.ShouldBe(1);
    }

    [Fact]
    public void A_previously_written_key_is_removed_when_its_value_becomes_non_positive()
    {
        const string existing = """{"agentUri":"http://a:5000","requestTimeoutMs":9000}""";

        var form = MTConnectDriverForm.MTConnectFormModel.FromJson(existing);
        form.RequestTimeoutMs = 0;

        // Not merely "not written" — the stale 9000 must not survive either, or the UI would show 0
        // while the stored config still said 9000.
        Emit(form, existing).ContainsKey("requestTimeoutMs").ShouldBeFalse();
    }

    // ---- the shape the driver actually binds ---------------------------------------------------

    [Fact]
    public void Emitted_config_binds_to_the_drivers_wire_dto_shape()
    {
        var json = MTConnectDriverForm.BuildConfigJson(
            new MTConnectDriverForm.MTConnectFormModel
            {
                AgentUri = "http://agent:5000",
                DeviceName = "VMC-1",
                RequestTimeoutMs = 7000,
            },
            null);

        // Mirror of MTConnectDriver.MTConnectDriverConfigDto (private there), asserting the emitted
        // document binds field-for-field — a rename on either side breaks this.
        var dto = JsonSerializer.Deserialize<WireDto>(json, _opts);

        dto.ShouldNotBeNull();
        dto!.AgentUri.ShouldBe("http://agent:5000");
        dto.DeviceName.ShouldBe("VMC-1");
        dto.RequestTimeoutMs.ShouldBe(7000);
        dto.SampleIntervalMs.ShouldBe(1000);
        dto.SampleCount.ShouldBe(1000);
        dto.HeartbeatMs.ShouldBe(10000);
        dto.Probe.ShouldNotBeNull();
        dto.Probe!.Enabled.ShouldBe(true);
        dto.Probe.IntervalMs.ShouldBe(5000);
        dto.Probe.TimeoutMs.ShouldBe(2000);
        dto.Reconnect.ShouldNotBeNull();
        dto.Reconnect!.MinBackoffMs.ShouldBe(0);
        dto.Reconnect.MaxBackoffMs.ShouldBe(30000);
        dto.Reconnect.BackoffMultiplier.ShouldBe(2.0);
    }

    private sealed class WireDto
    {
        public string? AgentUri { get; init; }
        public string? DeviceName { get; init; }
        public int? RequestTimeoutMs { get; init; }
        public int? SampleIntervalMs { get; init; }
        public int? SampleCount { get; init; }
        public int? HeartbeatMs { get; init; }
        public WireProbe? Probe { get; init; }
        public WireReconnect? Reconnect { get; init; }
    }

    private sealed class WireProbe
    {
        public bool? Enabled { get; init; }
        public int? IntervalMs { get; init; }
        public int? TimeoutMs { get; init; }
    }

    private sealed class WireReconnect
    {
        public int? MinBackoffMs { get; init; }
        public int? MaxBackoffMs { get; init; }
        public double? BackoffMultiplier { get; init; }
    }
}
