using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

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

/// <summary>
///     Verifies the Phase 7 Stream E entities (<see cref="Script"/>, <see cref="VirtualTag"/>,
///     <see cref="ScriptedAlarm"/>, <see cref="ScriptedAlarmState"/>) register correctly in
///     the EF model, map to the expected tables/columns/indexes, and carry the check constraints
///     the plan decisions call for. Introspection only — no SQL Server required.
/// </summary>
[Trait("Category", "Unit")]
public sealed class Phase7ScriptingEntitiesTests
{
    private static OtOpcUaConfigDbContext BuildCtx()
    {
        var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseSqlServer("Server=(local);Database=dummy;Integrated Security=true") // not connected
            .Options;
        return new OtOpcUaConfigDbContext(options);
    }

    private static Microsoft.EntityFrameworkCore.Metadata.IModel DesignModel(OtOpcUaConfigDbContext ctx)
        => ctx.GetService<IDesignTimeModel>().Model;

    /// <summary>Verifies that the Script entity is registered with the expected table name and columns.</summary>
    [Fact]
    public void Script_entity_registered_with_expected_table_and_columns()
    {
        using var ctx = BuildCtx();
        var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull();

        entity.GetTableName().ShouldBe("Script");
        entity.FindProperty(nameof(Script.ScriptRowId)).ShouldNotBeNull();
        entity.FindProperty(nameof(Script.ScriptId)).ShouldNotBeNull()
            .GetMaxLength().ShouldBe(64);
        entity.FindProperty(nameof(Script.SourceCode)).ShouldNotBeNull()
            .GetColumnType().ShouldBe("nvarchar(max)");
        entity.FindProperty(nameof(Script.SourceHash)).ShouldNotBeNull()
            .GetMaxLength().ShouldBe(64);
        entity.FindProperty(nameof(Script.Language)).ShouldNotBeNull()
            .GetMaxLength().ShouldBe(16);
    }

    /// <summary>Verifies that the Script entity has a unique logical ID constraint.</summary>
    [Fact]
    public void Script_has_unique_logical_id()
    {
        // v2 live-edit dropped the per-generation qualifier; Script.ScriptId is globally unique.
        using var ctx = BuildCtx();
        var entity = ctx.Model.FindEntityType(typeof(Script)).ShouldNotBeNull();
        entity.GetIndexes().Any(i => i.IsUnique).ShouldBeTrue("Script needs at least one unique index");
    }

    /// <summary>Verifies that the VirtualTag entity is registered with the expected trigger check constraint.</summary>
    [Fact]
    public void VirtualTag_entity_registered_with_trigger_check_constraint()
    {
        using var ctx = BuildCtx();
        var entity = DesignModel(ctx).FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
        entity.GetTableName().ShouldBe("VirtualTag");

        var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
        checks.ShouldContain("CK_VirtualTag_Trigger_AtLeastOne");
        checks.ShouldContain("CK_VirtualTag_TimerInterval_Min");
    }

    /// <summary>Verifies that the VirtualTag entity has a unique index on its logical key.</summary>
    [Fact]
    public void VirtualTag_has_unique_index_on_logical_key()
    {
        // v2 live-edit dropped the per-generation qualifier on VirtualTag's uniqueness.
        using var ctx = BuildCtx();
        var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
        entity.GetIndexes().Any(i => i.IsUnique).ShouldBeTrue("VirtualTag needs at least one unique index");
    }

    /// <summary>Verifies that the VirtualTag entity has ChangeTriggered, Historize, and TimerIntervalMs properties.</summary>
    [Fact]
    public void VirtualTag_has_ChangeTriggered_and_Historize_flags()
    {
        using var ctx = BuildCtx();
        var entity = ctx.Model.FindEntityType(typeof(VirtualTag)).ShouldNotBeNull();
        entity.FindProperty(nameof(VirtualTag.ChangeTriggered)).ShouldNotBeNull()
            .ClrType.ShouldBe(typeof(bool));
        entity.FindProperty(nameof(VirtualTag.Historize)).ShouldNotBeNull()
            .ClrType.ShouldBe(typeof(bool));
        entity.FindProperty(nameof(VirtualTag.TimerIntervalMs)).ShouldNotBeNull()
            .ClrType.ShouldBe(typeof(int?));
    }

    /// <summary>Verifies that the ScriptedAlarm entity is registered with severity and type check constraints.</summary>
    [Fact]
    public void ScriptedAlarm_entity_registered_with_severity_and_type_checks()
    {
        using var ctx = BuildCtx();
        var entity = DesignModel(ctx).FindEntityType(typeof(ScriptedAlarm)).ShouldNotBeNull();
        entity.GetTableName().ShouldBe("ScriptedAlarm");

        var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
        checks.ShouldContain("CK_ScriptedAlarm_Severity_Range");
        checks.ShouldContain("CK_ScriptedAlarm_AlarmType");
    }

    /// <summary>Verifies that ScriptedAlarm has HistorizeToAveva defaulted to true per plan decision 15.</summary>
    [Fact]
    public void ScriptedAlarm_has_HistorizeToAveva_default_true_per_plan_decision_15()
    {
        // Defaults live on the CLR default assignment — verify the initializer.
        var alarm = new ScriptedAlarm
        {
            ScriptedAlarmId = "a1",
            EquipmentId = "eq1",
            Name = "n",
            AlarmType = "LimitAlarm",
            MessageTemplate = "m",
            PredicateScriptId = "s1",
        };
        alarm.HistorizeToAveva.ShouldBeTrue();
        alarm.Retain.ShouldBeTrue();
        alarm.Severity.ShouldBe(500);
        alarm.Enabled.ShouldBeTrue();
    }

    /// <summary>Verifies that ScriptedAlarmState is keyed on ScriptedAlarmId and not generation-scoped.</summary>
    [Fact]
    public void ScriptedAlarmState_keyed_on_ScriptedAlarmId_not_generation_scoped()
    {
        using var ctx = BuildCtx();
        var entity = ctx.Model.FindEntityType(typeof(ScriptedAlarmState)).ShouldNotBeNull();
        entity.GetTableName().ShouldBe("ScriptedAlarmState");

        var pk = entity.FindPrimaryKey().ShouldNotBeNull();
        pk.Properties.Count.ShouldBe(1);
        pk.Properties[0].Name.ShouldBe(nameof(ScriptedAlarmState.ScriptedAlarmId));

        // State is NOT generation-scoped — GenerationId column should not exist per plan decision #14.
        entity.FindProperty("GenerationId").ShouldBeNull(
            "ack state follows alarm identity across generations");
    }

    /// <summary>Verifies that ScriptedAlarmState default values match Part 9 initial states.</summary>
    [Fact]
    public void ScriptedAlarmState_default_state_values_match_Part9_initial_states()
    {
        var state = new ScriptedAlarmState
        {
            ScriptedAlarmId = "a1",
            EnabledState = "Enabled",
            AckedState = "Unacknowledged",
            ConfirmedState = "Unconfirmed",
            ShelvingState = "Unshelved",
        };
        state.CommentsJson.ShouldBe("[]");
        state.LastAckUser.ShouldBeNull();
        state.LastAckUtc.ShouldBeNull();
    }

    /// <summary>Verifies that ScriptedAlarmState has a JSON check constraint on CommentsJson.</summary>
    [Fact]
    public void ScriptedAlarmState_has_JSON_check_constraint_on_CommentsJson()
    {
        using var ctx = BuildCtx();
        var entity = DesignModel(ctx).FindEntityType(typeof(ScriptedAlarmState)).ShouldNotBeNull();
        var checks = entity.GetCheckConstraints().Select(c => c.Name).ToArray();
        checks.ShouldContain("CK_ScriptedAlarmState_CommentsJson_IsJson");
    }

    /// <summary>Verifies that all new Phase 7 entities are exposed via DbSet properties.</summary>
    [Fact]
    public void All_new_entities_exposed_via_DbSet()
    {
        using var ctx = BuildCtx();
        ctx.Scripts.ShouldNotBeNull();
        ctx.VirtualTags.ShouldNotBeNull();
        ctx.ScriptedAlarms.ShouldNotBeNull();
        ctx.ScriptedAlarmStates.ShouldNotBeNull();
    }

    /// <summary>Verifies that the AddPhase7ScriptingTables migration exists in the assembly.</summary>
    [Fact]
    public void AddPhase7ScriptingTables_migration_exists_in_assembly()
    {
        // The migration type carries the design-time snapshot + Up/Down methods EF uses to
        // apply the schema. Missing = schema won't roll forward in deployments.
        var t = typeof(Migrations.AddPhase7ScriptingTables);
        t.ShouldNotBeNull();
    }
}
