using System.Text.Json;
using Bunit;
using Bunit.JSInterop;
using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;

/// <summary>
/// M9-T28b: the <c>AlarmTriggerEditor</c> and <c>ScriptTriggerEditor</c>
/// components must surface an Advisory|Strict selector for Expression triggers
/// and round-trip the <c>"analysisKind"</c> key into the trigger config JSON
/// that the T28a ValidationService backend reads.
/// </summary>

// ── AlarmTriggerConfigCodec — analysisKind parse / serialize ────────────────

public class AlarmTriggerAnalysisKindCodecTests
{
    // Codec: parse — absent key defaults to Advisory (false)
    [Fact]
    public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
    {
        var json = @"{""expression"":""Attributes[""Temp""] > 50""}";
        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
        Assert.False(model.IsStrictAnalysisKind);
    }

    // Codec: parse — "Advisory" explicit → false
    [Fact]
    public void Parse_Expression_AdvisoryKey_ReturnsFalse()
    {
        var json = @"{""expression"":""x > 0"",""analysisKind"":""Advisory""}";
        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
        Assert.False(model.IsStrictAnalysisKind);
    }

    // Codec: parse — "Strict" → true
    [Fact]
    public void Parse_Expression_StrictKey_ReturnsTrue()
    {
        var json = @"{""expression"":""x > 0"",""analysisKind"":""Strict""}";
        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
        Assert.True(model.IsStrictAnalysisKind);
    }

    // Codec: parse — case-insensitive
    [Fact]
    public void Parse_Expression_StrictKeyCaseInsensitive()
    {
        var json = @"{""expression"":""x > 0"",""analysisKind"":""strict""}";
        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);
        Assert.True(model.IsStrictAnalysisKind);
    }

    // Codec: serialize — Advisory (false) → "analysisKind" key omitted
    [Fact]
    public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
    {
        var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);

        using var doc = JsonDocument.Parse(json);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }

    // Codec: serialize — Strict (true) → "analysisKind":"Strict"
    [Fact]
    public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
    {
        var model = new AlarmTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.Expression);

        using var doc = JsonDocument.Parse(json);
        Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
    }

    // Codec: serialize — Strict not emitted for non-Expression trigger types
    [Fact]
    public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Min = 0,
            Max = 100,
            IsStrictAnalysisKind = true   // should be ignored for non-Expression
        };
        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);

        using var doc = JsonDocument.Parse(json);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }

    // Codec: round-trip Strict
    [Fact]
    public void RoundTrip_Expression_Strict_Preserved()
    {
        var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = true };
        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);

        Assert.True(round.IsStrictAnalysisKind);
        Assert.Equal("Temp > 80", round.Expression);
    }

    // Codec: round-trip Advisory (omit + re-parse → still false)
    [Fact]
    public void RoundTrip_Expression_Advisory_Preserved()
    {
        var original = new AlarmTriggerModel { Expression = "Temp > 80", IsStrictAnalysisKind = false };
        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.Expression);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.Expression);

        Assert.False(round.IsStrictAnalysisKind);
    }
}

// ── ScriptTriggerConfigCodec — analysisKind parse / serialize ───────────────

public class ScriptTriggerAnalysisKindCodecTests
{
    // Codec: parse — absent key defaults to Advisory (false)
    [Fact]
    public void Parse_Expression_NoAnalysisKind_DefaultsToAdvisory()
    {
        var json = @"{""expression"":""x > 0"",""mode"":""OnTrue""}";
        var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
        Assert.False(model.IsStrictAnalysisKind);
    }

    // Codec: parse — "Strict" → true
    [Fact]
    public void Parse_Expression_StrictKey_ReturnsTrue()
    {
        var json = @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}";
        var model = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);
        Assert.True(model.IsStrictAnalysisKind);
    }

    // Codec: serialize — Advisory → key omitted
    [Fact]
    public void Serialize_Expression_Advisory_OmitsAnalysisKindKey()
    {
        var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = false };
        var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;

        using var doc = JsonDocument.Parse(json);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }

    // Codec: serialize — Strict → "analysisKind":"Strict"
    [Fact]
    public void Serialize_Expression_Strict_WritesAnalysisKindStrict()
    {
        var model = new ScriptTriggerModel { Expression = "x > 0", IsStrictAnalysisKind = true };
        var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Expression)!;

        using var doc = JsonDocument.Parse(json);
        Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
    }

    // Codec: serialize — Strict not emitted for non-Expression triggers
    [Fact]
    public void Serialize_NonExpression_DoesNotEmitAnalysisKind()
    {
        var model = new ScriptTriggerModel
        {
            AttributeName = "Temp",
            Operator = ">",
            Threshold = 50,
            IsStrictAnalysisKind = true   // must be ignored for Conditional
        };
        var json = ScriptTriggerConfigCodec.Serialize(model, ScriptTriggerKind.Conditional)!;

        using var doc = JsonDocument.Parse(json);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }

    // Codec: round-trip Strict
    [Fact]
    public void RoundTrip_Expression_Strict_Preserved()
    {
        var original = new ScriptTriggerModel
        {
            Expression = "Temp > 80",
            Mode = ScriptTriggerMode.WhileTrue,
            IsStrictAnalysisKind = true
        };
        var json = ScriptTriggerConfigCodec.Serialize(original, ScriptTriggerKind.Expression)!;
        var round = ScriptTriggerConfigCodec.Parse(json, ScriptTriggerKind.Expression);

        Assert.True(round.IsStrictAnalysisKind);
        Assert.Equal(ScriptTriggerMode.WhileTrue, round.Mode);
    }
}

// ── AlarmTriggerEditor — UI selector (bUnit component tests) ─────────────────

public class AlarmTriggerEditorAnalysisKindTests : BunitContext
{
    public AlarmTriggerEditorAnalysisKindTests()
    {
        // MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
        // mode so the editor renders without requiring a full setup for each call.
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    // Selector is visible for Expression trigger type
    [Fact]
    public void AlarmTriggerEditor_Expression_ShowsAnalysisKindSelector()
    {
        var cut = Render<AlarmTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, AlarmTriggerType.Expression)
            .Add(p => p.Value, @"{""expression"":""""}"));

        // The selector must exist and have id="alarm-trigger-kind"
        var select = cut.Find("#alarm-trigger-kind");
        Assert.NotNull(select);
    }

    // Selector is NOT visible for non-Expression trigger types
    [Fact]
    public void AlarmTriggerEditor_NonExpression_DoesNotShowAnalysisKindSelector()
    {
        var cut = Render<AlarmTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, AlarmTriggerType.RangeViolation)
            .Add(p => p.Value, @"{""attributeName"":""Temp"",""min"":0,""max"":100}"));

        Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#alarm-trigger-kind"));
    }

    // Selector defaults to Advisory
    [Fact]
    public void AlarmTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
    {
        var cut = Render<AlarmTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, AlarmTriggerType.Expression)
            .Add(p => p.Value, @"{""expression"":""x > 0""}"));

        var select = cut.Find("#alarm-trigger-kind");
        // The bound value must be "Advisory" — not just present in the HTML
        Assert.Equal("Advisory", select.GetAttribute("value"));
    }

    // Selector defaults to Advisory when config has no analysisKind (ScriptTriggerEditor)
    [Fact]
    public void ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory()
    {
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Expression")
            .Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}"));

        var select = cut.Find("#script-trigger-kind");
        // The bound value must be "Advisory" — not just present in the HTML
        Assert.Equal("Advisory", select.GetAttribute("value"));
    }

    // Choosing Strict writes analysisKind:"Strict" into emitted config
    [Fact]
    public void AlarmTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
    {
        string? emitted = null;
        var cut = Render<AlarmTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, AlarmTriggerType.Expression)
            .Add(p => p.Value, @"{""expression"":""x > 0""}")
            .Add(p => p.ValueChanged,
                EventCallback.Factory.Create<string?>(this, v => emitted = v)));

        cut.Find("#alarm-trigger-kind").Change("Strict");

        Assert.NotNull(emitted);
        using var doc = JsonDocument.Parse(emitted!);
        Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
    }

    // Loaded Strict config reflects in selector, emits Strict on next change
    [Fact]
    public void AlarmTriggerEditor_Expression_LoadedStrictConfig_RetainedOnEdit()
    {
        string? emitted = null;
        var cut = Render<AlarmTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, AlarmTriggerType.Expression)
            .Add(p => p.Value, @"{""expression"":""x > 0"",""analysisKind"":""Strict""}")
            .Add(p => p.ValueChanged,
                EventCallback.Factory.Create<string?>(this, v => emitted = v)));

        // Switch to Advisory — config must no longer carry analysisKind
        cut.Find("#alarm-trigger-kind").Change("Advisory");

        Assert.NotNull(emitted);
        using var doc = JsonDocument.Parse(emitted!);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }
}

// ── ScriptTriggerEditor — UI selector (bUnit component tests) ────────────────

public class ScriptTriggerEditorAnalysisKindTests : BunitContext
{
    public ScriptTriggerEditorAnalysisKindTests()
    {
        // MonacoEditor calls MonacoBlazor.createEditor via JS interop. Use Loose
        // mode so the editor renders without requiring a full setup for each call.
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    // Selector is visible for Expression trigger type
    [Fact]
    public void ScriptTriggerEditor_Expression_ShowsAnalysisKindSelector()
    {
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Expression")
            .Add(p => p.TriggerConfig, @"{""expression"":"""",""mode"":""OnTrue""}"));

        var select = cut.Find("#script-trigger-kind");
        Assert.NotNull(select);
    }

    // Selector is NOT visible for non-Expression trigger types
    [Fact]
    public void ScriptTriggerEditor_Conditional_DoesNotShowAnalysisKindSelector()
    {
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Conditional")
            .Add(p => p.TriggerConfig, @"{""attributeName"":""Temp"",""operator"":"">"",""threshold"":50,""mode"":""OnTrue""}"));

        Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find("#script-trigger-kind"));
    }

    // Choosing Strict writes analysisKind:"Strict" into emitted config
    [Fact]
    public void ScriptTriggerEditor_Expression_ChoosingStrict_EmitsAnalysisKindStrict()
    {
        ScriptTriggerValue? captured = null;
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Expression")
            .Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}")
            .Add(p => p.Changed,
                EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));

        cut.Find("#script-trigger-kind").Change("Strict");

        Assert.NotNull(captured);
        using var doc = JsonDocument.Parse(captured!.Config!);
        Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString());
    }

    // Choosing Advisory emits config without analysisKind key
    [Fact]
    public void ScriptTriggerEditor_Expression_ChoosingAdvisory_OmitsAnalysisKindKey()
    {
        ScriptTriggerValue? captured = null;
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Expression")
            .Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
            .Add(p => p.Changed,
                EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));

        cut.Find("#script-trigger-kind").Change("Advisory");

        Assert.NotNull(captured);
        using var doc = JsonDocument.Parse(captured!.Config!);
        Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _));
    }

    // Loaded Strict is retained on unrelated edit (fire mode change)
    [Fact]
    public void ScriptTriggerEditor_Expression_LoadedStrict_RetainedOnModeChange()
    {
        ScriptTriggerValue? captured = null;
        var cut = Render<ScriptTriggerEditor>(ps => ps
            .Add(p => p.TriggerType, "Expression")
            .Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue"",""analysisKind"":""Strict""}")
            .Add(p => p.Changed,
                EventCallback.Factory.Create<ScriptTriggerValue>(this, v => captured = v)));

        // Change fire mode — Strict kind must survive
        cut.Find("#script-trigger-mode").Change("WhileTrue");

        Assert.NotNull(captured);
        Assert.Contains("\"analysisKind\":\"Strict\"", captured!.Config);
    }
}
