using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;

namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Serialization;

public sealed class EntitySerializerTests
{
    private static EntityAggregate MakeEmptyAggregate() => new(
        TemplateFolders: Array.Empty<TemplateFolder>(),
        Templates: Array.Empty<Template>(),
        SharedScripts: Array.Empty<SharedScript>(),
        ExternalSystems: Array.Empty<ExternalSystemDefinition>(),
        ExternalSystemMethods: Array.Empty<ExternalSystemMethod>(),
        DatabaseConnections: Array.Empty<DatabaseConnectionDefinition>(),
        NotificationLists: Array.Empty<NotificationList>(),
        SmtpConfigurations: Array.Empty<SmtpConfiguration>(),
        ApiMethods: Array.Empty<ApiMethod>());

    [Fact]
    public void ToDto_carves_external_system_credentials_into_secrets_block()
    {
        var sys = new ExternalSystemDefinition("erp", "https://erp/api", "ApiKey")
        {
            Id = 1,
            AuthConfiguration = "{\"apiKey\":\"super-secret\"}",
        };
        var aggregate = MakeEmptyAggregate() with { ExternalSystems = new[] { sys } };

        var sut = new EntitySerializer();
        var dto = sut.ToBundleContent(aggregate);

        var dtoSys = Assert.Single(dto.ExternalSystems);
        Assert.NotNull(dtoSys.Secrets);
        Assert.True(dtoSys.Secrets!.Values.ContainsKey("AuthConfiguration"));
        Assert.Equal("{\"apiKey\":\"super-secret\"}", dtoSys.Secrets.Values["AuthConfiguration"]);
        // Public part does not carry the secret.
        Assert.Equal("erp", dtoSys.Name);
        Assert.Equal("https://erp/api", dtoSys.BaseUrl);
        Assert.Equal("ApiKey", dtoSys.AuthType);
    }

    [Fact]
    public void ToDto_carves_smtp_password_into_secrets_block()
    {
        var smtp = new SmtpConfiguration("smtp.example.com", "Basic", "noreply@example.com")
        {
            Id = 1,
            Port = 587,
            Credentials = "user:p@ssw0rd",
        };
        var aggregate = MakeEmptyAggregate() with { SmtpConfigurations = new[] { smtp } };

        var dto = new EntitySerializer().ToBundleContent(aggregate);

        var dtoSmtp = Assert.Single(dto.SmtpConfigs);
        Assert.NotNull(dtoSmtp.Secrets);
        Assert.Equal("user:p@ssw0rd", dtoSmtp.Secrets!.Values["Credentials"]);
        Assert.Equal("smtp.example.com", dtoSmtp.Host);
        Assert.Equal(587, dtoSmtp.Port);
    }

    [Fact]
    public void Roundtrip_template_preserves_attributes_alarms_scripts_composition()
    {
        var folder = new TemplateFolder("root") { Id = 1, SortOrder = 0 };
        var basic = new Template("Basic") { Id = 1, FolderId = 1, Description = "base" };
        basic.Attributes.Add(new TemplateAttribute("Pressure")
        {
            Id = 1,
            TemplateId = 1,
            DataType = DataType.Double,
            Value = "0",
            IsLocked = true,
            Description = "PSI",
        });
        basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
        {
            Id = 1,
            TemplateId = 1,
            TriggerType = "Periodic",
            ParameterDefinitions = "[]",
            ReturnDefinition = "void",
            IsLocked = false,
            MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
        });
        basic.Alarms.Add(new TemplateAlarm("High")
        {
            Id = 1,
            TemplateId = 1,
            PriorityLevel = 2,
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"threshold\":100}",
            IsLocked = false,
            // FU-B / #37 — alarm fires "OnUpdate" script when triggered. The FK is
            // an in-aggregate script id; the DTO carries the name and the
            // importer resolves the FK on the way back in.
            OnTriggerScriptId = 1,
        });

        var assembly = new Template("Assembly") { Id = 2, FolderId = 1 };
        assembly.Compositions.Add(new TemplateComposition("MotorA")
        {
            Id = 1,
            TemplateId = 2,
            ComposedTemplateId = 1, // refers to "Basic".
        });

        var aggregate = MakeEmptyAggregate() with
        {
            TemplateFolders = new[] { folder },
            Templates = new[] { basic, assembly },
        };

        var sut = new EntitySerializer();
        var dto = sut.ToBundleContent(aggregate);

        // FU-B / #37 — verify the DTO carries OnTriggerScriptName by NAME (not id).
        // The bundle is portable across environments so a script-id FK can't
        // survive a round-trip; resolution back to a script id is the importer's
        // job once the parent template's scripts have been re-persisted.
        var dtoBasic = Assert.Single(dto.Templates, t => t.Name == "Basic");
        var dtoAlarm = Assert.Single(dtoBasic.Alarms);
        Assert.Equal("OnUpdate", dtoAlarm.OnTriggerScriptName);

        var roundTripped = sut.FromBundleContent(dto);

        var rtBasic = Assert.Single(roundTripped.Templates, t => t.Name == "Basic");
        var rtAttr = Assert.Single(rtBasic.Attributes);
        Assert.Equal("Pressure", rtAttr.Name);
        Assert.Equal(DataType.Double, rtAttr.DataType);
        Assert.Equal("0", rtAttr.Value);
        Assert.True(rtAttr.IsLocked);

        var rtAlarm = Assert.Single(rtBasic.Alarms);
        Assert.Equal("High", rtAlarm.Name);
        Assert.Equal(AlarmTriggerType.RangeViolation, rtAlarm.TriggerType);
        Assert.Equal("{\"threshold\":100}", rtAlarm.TriggerConfiguration);
        Assert.Equal(2, rtAlarm.PriorityLevel);
        // FromBundleContent leaves OnTriggerScriptId null — the importer
        // resolves it post-persist when target-DB script ids are known.
        Assert.Null(rtAlarm.OnTriggerScriptId);

        var rtScript = Assert.Single(rtBasic.Scripts);
        Assert.Equal("OnUpdate", rtScript.Name);
        Assert.Equal("return 1;", rtScript.Code);
        Assert.Equal("Periodic", rtScript.TriggerType);
        Assert.Equal(TimeSpan.FromSeconds(30), rtScript.MinTimeBetweenRuns);

        var rtAssembly = Assert.Single(roundTripped.Templates, t => t.Name == "Assembly");
        var rtComp = Assert.Single(rtAssembly.Compositions);
        Assert.Equal("MotorA", rtComp.InstanceName);
    }

    [Fact]
    public void Roundtrip_template_folder_preserves_hierarchy()
    {
        var root = new TemplateFolder("Root") { Id = 1, SortOrder = 0 };
        var child = new TemplateFolder("Pumps") { Id = 2, ParentFolderId = 1, SortOrder = 1 };

        var aggregate = MakeEmptyAggregate() with
        {
            TemplateFolders = new[] { root, child },
        };

        var sut = new EntitySerializer();
        var dto = sut.ToBundleContent(aggregate);
        var rt = sut.FromBundleContent(dto);

        Assert.Equal(2, rt.TemplateFolders.Count);
        var rtRoot = Assert.Single(rt.TemplateFolders, f => f.Name == "Root");
        var rtChild = Assert.Single(rt.TemplateFolders, f => f.Name == "Pumps");
        Assert.Null(rtRoot.ParentFolderId);
        // Hierarchy is preserved by name reference; new local ids get assigned but
        // the child's parent must still point at the row whose name is "Root".
        Assert.NotNull(rtChild.ParentFolderId);
        Assert.Equal(rtRoot.Id, rtChild.ParentFolderId);
    }

    [Fact]
    public void Roundtrip_external_system_preserves_retry_config()
    {
        var sys = new ExternalSystemDefinition("billing", "https://billing/api", "Basic")
        {
            Id = 1,
            MaxRetries = 5,
            RetryDelay = TimeSpan.FromSeconds(15),
        };
        var aggregate = MakeEmptyAggregate() with { ExternalSystems = new[] { sys } };

        var sut = new EntitySerializer();
        var dto = sut.ToBundleContent(aggregate);
        var roundTripped = sut.FromBundleContent(dto);

        var rtSys = Assert.Single(roundTripped.ExternalSystems);
        Assert.Equal("billing", rtSys.Name);
        Assert.Equal(5, rtSys.MaxRetries);
        Assert.Equal(TimeSpan.FromSeconds(15), rtSys.RetryDelay);
    }

    [Fact]
    public void FromDto_with_null_SecretsBlock_yields_entity_with_default_empty_secrets()
    {
        var dto = new BundleContentDto(
            TemplateFolders: Array.Empty<TemplateFolderDto>(),
            Templates: Array.Empty<TemplateDto>(),
            SharedScripts: Array.Empty<SharedScriptDto>(),
            ExternalSystems: new[]
            {
                new ExternalSystemDto("erp", "https://x", "None", MaxRetries: 3, RetryDelay: TimeSpan.FromSeconds(5), Array.Empty<ExternalSystemMethodDto>(), Secrets: null),
            },
            DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
            NotificationLists: Array.Empty<NotificationListDto>(),
            SmtpConfigs: Array.Empty<SmtpConfigDto>(),
            ApiMethods: Array.Empty<ApiMethodDto>());

        var aggregate = new EntitySerializer().FromBundleContent(dto);

        var sys = Assert.Single(aggregate.ExternalSystems);
        Assert.Null(sys.AuthConfiguration);
    }
}
