using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Admin.Services;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;

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

/// <summary>
///     Admin-side services shipped in Phase 7 Stream F — draft CRUD for scripts + virtual
///     tags + scripted alarms, the pre-publish test harness, and the historian
///     diagnostics façade.
/// </summary>
[Trait("Category", "Unit")]
public sealed class Phase7ServicesTests
{
    private static OtOpcUaConfigDbContext NewDb([System.Runtime.CompilerServices.CallerMemberName] string test = "")
    {
        var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseInMemoryDatabase($"phase7-{test}-{Guid.NewGuid():N}")
            .Options;
        return new OtOpcUaConfigDbContext(options);
    }

    [Fact]
    public async Task ScriptService_AddAsync_generates_logical_id_and_hash()
    {
        using var db = NewDb();
        var svc = new ScriptService(db);

        var s = await svc.AddAsync(5, "line-rate", "return ctx.GetTag(\"a\").Value;", default);

        s.ScriptId.ShouldStartWith("scr-");
        s.GenerationId.ShouldBe(5);
        s.SourceHash.Length.ShouldBe(64);
        (await svc.ListAsync(5, default)).Count.ShouldBe(1);
    }

    [Fact]
    public async Task ScriptService_UpdateAsync_recomputes_hash_on_source_change()
    {
        using var db = NewDb();
        var svc = new ScriptService(db);
        var s = await svc.AddAsync(5, "s", "return 1;", default);
        var hashBefore = s.SourceHash;

        var updated = await svc.UpdateAsync(5, s.ScriptId, "s", "return 2;", default);
        updated.SourceHash.ShouldNotBe(hashBefore);
    }

    [Fact]
    public async Task ScriptService_UpdateAsync_same_source_same_hash()
    {
        using var db = NewDb();
        var svc = new ScriptService(db);
        var s = await svc.AddAsync(5, "s", "return 1;", default);
        var updated = await svc.UpdateAsync(5, s.ScriptId, "renamed", "return 1;", default);

        updated.SourceHash.ShouldBe(s.SourceHash, "source unchanged → hash unchanged → compile cache hit preserved");
    }

    [Fact]
    public async Task ScriptService_DeleteAsync_is_idempotent()
    {
        using var db = NewDb();
        var svc = new ScriptService(db);

        await Should.NotThrowAsync(() => svc.DeleteAsync(5, "nonexistent", default));
    }

    [Fact]
    public async Task VirtualTagService_round_trips_trigger_flags()
    {
        using var db = NewDb();
        var svc = new VirtualTagService(db);

        var v = await svc.AddAsync(7, "eq-1", "LineRate", "Float32", "scr-1",
            changeTriggered: true, timerIntervalMs: 1000, historize: true, default);

        v.ChangeTriggered.ShouldBeTrue();
        v.TimerIntervalMs.ShouldBe(1000);
        v.Historize.ShouldBeTrue();
        v.Enabled.ShouldBeTrue();
        (await svc.ListAsync(7, default)).Single().VirtualTagId.ShouldBe(v.VirtualTagId);
    }

    [Fact]
    public async Task VirtualTagService_update_enabled_toggles_flag()
    {
        using var db = NewDb();
        var svc = new VirtualTagService(db);
        var v = await svc.AddAsync(7, "eq-1", "N", "Int32", "scr-1", true, null, false, default);

        var disabled = await svc.UpdateEnabledAsync(7, v.VirtualTagId, false, default);
        disabled.Enabled.ShouldBeFalse();
    }

    [Fact]
    public async Task ScriptedAlarmService_defaults_HistorizeToAveva_true_per_plan_decision_15()
    {
        using var db = NewDb();
        var svc = new ScriptedAlarmService(db);

        var a = await svc.AddAsync(9, "eq-1", "HighTemp", "LimitAlarm", severity: 800,
            messageTemplate: "{Temp} too high", predicateScriptId: "scr-9",
            historizeToAveva: true, retain: true, default);

        a.HistorizeToAveva.ShouldBeTrue();
        a.Severity.ShouldBe(800);
        a.ScriptedAlarmId.ShouldStartWith("sal-");
    }

    [Fact]
    public async Task ScriptTestHarness_runs_successful_script_and_captures_writes()
    {
        var harness = new ScriptTestHarnessService();
        var source = """
            ctx.SetVirtualTag("Out", 42);
            return ctx.GetTag("In").Value;
            """;
        var inputs = new Dictionary<string, DataValueSnapshot>
        {
            ["In"] = new(123, 0u, DateTime.UtcNow, DateTime.UtcNow),
        };

        var result = await harness.RunVirtualTagAsync(source, inputs, default);

        result.Outcome.ShouldBe(ScriptTestOutcome.Success);
        result.Output.ShouldBe(123);
        result.Writes["Out"].ShouldBe(42);
    }

    [Fact]
    public async Task ScriptTestHarness_rejects_missing_synthetic_input()
    {
        var harness = new ScriptTestHarnessService();
        var source = """return ctx.GetTag("A").Value;""";

        var result = await harness.RunVirtualTagAsync(source, new Dictionary<string, DataValueSnapshot>(), default);

        result.Outcome.ShouldBe(ScriptTestOutcome.MissingInputs);
        result.Errors[0].ShouldContain("A");
    }

    [Fact]
    public async Task ScriptTestHarness_rejects_extra_synthetic_input_not_referenced_by_script()
    {
        var harness = new ScriptTestHarnessService();
        var source = """return 1;""";  // no GetTag calls
        var inputs = new Dictionary<string, DataValueSnapshot>
        {
            ["Unexpected"] = new(0, 0u, DateTime.UtcNow, DateTime.UtcNow),
        };

        var result = await harness.RunVirtualTagAsync(source, inputs, default);

        result.Outcome.ShouldBe(ScriptTestOutcome.UnknownInputs);
        result.Errors[0].ShouldContain("Unexpected");
    }

    [Fact]
    public async Task ScriptTestHarness_rejects_non_literal_path()
    {
        var harness = new ScriptTestHarnessService();
        var source = """
            var p = "A";
            return ctx.GetTag(p).Value;
            """;

        var result = await harness.RunVirtualTagAsync(source, new Dictionary<string, DataValueSnapshot>(), default);

        result.Outcome.ShouldBe(ScriptTestOutcome.DependencyRejected);
        result.Errors.ShouldNotBeEmpty();
    }

    [Fact]
    public async Task ScriptTestHarness_surfaces_compile_error_as_Threw()
    {
        var harness = new ScriptTestHarnessService();
        var source = "this is not valid C#;";

        var result = await harness.RunVirtualTagAsync(source, new Dictionary<string, DataValueSnapshot>(), default);

        result.Outcome.ShouldBe(ScriptTestOutcome.Threw);
    }

    [Fact]
    public void HistorianDiagnosticsService_reports_Disabled_for_null_sink()
    {
        var diag = new HistorianDiagnosticsService(NullAlarmHistorianSink.Instance);
        diag.GetStatus().DrainState.ShouldBe(HistorianDrainState.Disabled);
        diag.TryRetryDeadLettered().ShouldBe(0);
    }
}
