using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
using ZB.MOM.WW.ScadaBridge.Transport.Export;

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

public sealed class DependencyResolverTests
{
    private readonly ITemplateEngineRepository _templates = Substitute.For<ITemplateEngineRepository>();
    private readonly IExternalSystemRepository _externalSystems = Substitute.For<IExternalSystemRepository>();
    private readonly INotificationRepository _notifications = Substitute.For<INotificationRepository>();
    private readonly IInboundApiRepository _inboundApi = Substitute.For<IInboundApiRepository>();
    private readonly ISiteRepository _sites = Substitute.For<ISiteRepository>();

    private DependencyResolver Sut() => new(_templates, _externalSystems, _notifications, _inboundApi, _sites);

    private static ExportSelection SelectTemplates(params int[] ids) => new(
        TemplateIds: ids,
        SharedScriptIds: Array.Empty<int>(),
        ExternalSystemIds: Array.Empty<int>(),
        DatabaseConnectionIds: Array.Empty<int>(),
        NotificationListIds: Array.Empty<int>(),
        SmtpConfigurationIds: Array.Empty<int>(),
        ApiMethodIds: Array.Empty<int>(),
        IncludeDependencies: true);

    private static ExportSelection SelectApiMethods(params int[] ids) => new(
        TemplateIds: Array.Empty<int>(),
        SharedScriptIds: Array.Empty<int>(),
        ExternalSystemIds: Array.Empty<int>(),
        NotificationListIds: Array.Empty<int>(),
        DatabaseConnectionIds: Array.Empty<int>(),
        SmtpConfigurationIds: Array.Empty<int>(),
        ApiMethodIds: ids,
        IncludeDependencies: true);

    private static ExportSelection SelectSites(int[] siteIds, bool includeDeps = true) => new(
        TemplateIds: Array.Empty<int>(),
        SharedScriptIds: Array.Empty<int>(),
        ExternalSystemIds: Array.Empty<int>(),
        DatabaseConnectionIds: Array.Empty<int>(),
        NotificationListIds: Array.Empty<int>(),
        SmtpConfigurationIds: Array.Empty<int>(),
        ApiMethodIds: Array.Empty<int>(),
        IncludeDependencies: includeDeps,
        SiteIds: siteIds);

    private static ExportSelection SelectInstances(int[] instanceIds, bool includeDeps = true, int[]? siteIds = null) => new(
        TemplateIds: Array.Empty<int>(),
        SharedScriptIds: Array.Empty<int>(),
        ExternalSystemIds: Array.Empty<int>(),
        DatabaseConnectionIds: Array.Empty<int>(),
        NotificationListIds: Array.Empty<int>(),
        SmtpConfigurationIds: Array.Empty<int>(),
        ApiMethodIds: Array.Empty<int>(),
        IncludeDependencies: includeDeps,
        SiteIds: siteIds ?? Array.Empty<int>(),
        InstanceIds: instanceIds);

    private static ExportSelection SelectSmsConfigs(params int[] ids) => new(
        TemplateIds: Array.Empty<int>(),
        SharedScriptIds: Array.Empty<int>(),
        ExternalSystemIds: Array.Empty<int>(),
        DatabaseConnectionIds: Array.Empty<int>(),
        NotificationListIds: Array.Empty<int>(),
        SmtpConfigurationIds: Array.Empty<int>(),
        ApiMethodIds: Array.Empty<int>(),
        IncludeDependencies: true,
        SmsConfigurationIds: ids);

    private void StubTemplate(Template t)
    {
        _templates.GetTemplateWithChildrenAsync(t.Id, Arg.Any<CancellationToken>()).Returns(t);
    }

    private void StubAllSharedScripts(params SharedScript[] scripts)
    {
        _templates.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>()).Returns(scripts);
    }

    private void StubAllExternalSystems(params ExternalSystemDefinition[] systems)
    {
        _externalSystems.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>()).Returns(systems);
        foreach (var es in systems)
        {
            _externalSystems
                .GetMethodsByExternalSystemIdAsync(es.Id, Arg.Any<CancellationToken>())
                .Returns(Array.Empty<ExternalSystemMethod>());
        }
    }

    private void StubAllFolders(params TemplateFolder[] folders)
    {
        _templates.GetAllFoldersAsync(Arg.Any<CancellationToken>()).Returns(folders);
    }

    private void StubSite(Site site)
    {
        _sites.GetSiteByIdAsync(site.Id, Arg.Any<CancellationToken>()).Returns(site);
    }

    private void StubSiteConnections(int siteId, params DataConnection[] connections)
    {
        _sites.GetDataConnectionsBySiteIdAsync(siteId, Arg.Any<CancellationToken>()).Returns(connections);
    }

    private void StubSiteInstances(int siteId, params Instance[] instances)
    {
        _sites.GetInstancesBySiteIdAsync(siteId, Arg.Any<CancellationToken>()).Returns(instances);
    }

    // Stubs the per-instance child getters on the template repo so the resolver can
    // load an instance + its (possibly empty) override/binding collections by id.
    private void StubInstance(Instance instance)
    {
        _templates.GetInstanceByIdAsync(instance.Id, Arg.Any<CancellationToken>()).Returns(instance);
        _templates.GetOverridesByInstanceIdAsync(instance.Id, Arg.Any<CancellationToken>())
            .Returns(instance.AttributeOverrides.ToList());
        _templates.GetAlarmOverridesByInstanceIdAsync(instance.Id, Arg.Any<CancellationToken>())
            .Returns(instance.AlarmOverrides.ToList());
        _templates.GetNativeAlarmSourceOverridesByInstanceIdAsync(instance.Id, Arg.Any<CancellationToken>())
            .Returns(instance.NativeAlarmSourceOverrides.ToList());
        _templates.GetBindingsByInstanceIdAsync(instance.Id, Arg.Any<CancellationToken>())
            .Returns(instance.ConnectionBindings.ToList());
    }

    [Fact]
    public async Task Resolve_includes_base_template_for_composed_template()
    {
        var baseT = new Template("Base") { Id = 10 };
        var composing = new Template("Top") { Id = 11 };
        composing.Compositions.Add(new TemplateComposition("slot") { TemplateId = 11, ComposedTemplateId = 10 });

        StubTemplate(composing);
        StubTemplate(baseT);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectTemplates(11), CancellationToken.None);

        Assert.Equal(2, result.Templates.Count);
        Assert.Contains(result.Templates, t => t.Id == 10);
        Assert.Contains(result.Templates, t => t.Id == 11);
    }

    [Fact]
    public async Task Resolve_includes_shared_script_referenced_by_template()
    {
        var shared = new SharedScript("UtilHelper", "return 42;") { Id = 100 };
        var t = new Template("UsesUtil") { Id = 1 };
        t.Scripts.Add(new TemplateScript("body", "var x = UtilHelper(); return x;") { TemplateId = 1 });

        StubTemplate(t);
        StubAllSharedScripts(shared, new SharedScript("OtherScript", "return 0;") { Id = 101 });
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectTemplates(1), CancellationToken.None);

        Assert.Single(result.SharedScripts);
        Assert.Equal("UtilHelper", result.SharedScripts[0].Name);
    }

    [Fact]
    public async Task Resolve_includes_external_system_referenced_by_template()
    {
        var es = new ExternalSystemDefinition("ErpSystem", "https://erp", "ApiKey") { Id = 7 };
        var t = new Template("UsesErp") { Id = 2 };
        t.Scripts.Add(new TemplateScript("call", "ErpSystem.Call(\"x\");") { TemplateId = 2 });

        StubTemplate(t);
        StubAllSharedScripts();
        StubAllExternalSystems(es, new ExternalSystemDefinition("Other", "https://o", "Basic") { Id = 8 });
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectTemplates(2), CancellationToken.None);

        Assert.Single(result.ExternalSystems);
        Assert.Equal("ErpSystem", result.ExternalSystems[0].Name);
    }

    [Fact]
    public async Task Resolve_includes_api_method_shared_script_dependency()
    {
        var shared = new SharedScript("Validator", "return true;") { Id = 50 };
        var method = new ApiMethod("submit", "var ok = Validator(input); return ok;") { Id = 5 };

        _inboundApi.GetApiMethodByIdAsync(5, Arg.Any<CancellationToken>()).Returns(method);
        StubAllSharedScripts(shared);
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectApiMethods(5), CancellationToken.None);

        Assert.Single(result.ApiMethods);
        Assert.Single(result.SharedScripts);
        Assert.Equal("Validator", result.SharedScripts[0].Name);
    }

    [Fact]
    public async Task Resolve_includes_selected_sms_configs()
    {
        // Mirrors the SMTP/by-id seed path: a selected SMS config id resolves via
        // GetSmsConfigurationByIdAsync into ResolvedExport.SmsConfigs, and a manifest
        // row (keyed by AccountSid) is emitted alongside it.
        var sms = new SmsConfiguration("AC_test_sid", "+15551230000") { Id = 7 };
        _notifications.GetSmsConfigurationByIdAsync(7, Arg.Any<CancellationToken>()).Returns(sms);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectSmsConfigs(7), CancellationToken.None);

        Assert.Single(result.SmsConfigs);
        Assert.Equal("AC_test_sid", result.SmsConfigs[0].AccountSid);
        Assert.Contains(result.ContentManifest,
            e => e.Type == "SmsConfiguration" && e.Name == "AC_test_sid");
    }

    [Fact]
    public async Task Resolve_handles_diamond_dependency_without_duplication()
    {
        // A composes B and C; both B and C compose D. Selection={A}. D must appear once.
        var d = new Template("D") { Id = 4 };
        var b = new Template("B") { Id = 2 };
        b.Compositions.Add(new TemplateComposition("d-in-b") { TemplateId = 2, ComposedTemplateId = 4 });
        var c = new Template("C") { Id = 3 };
        c.Compositions.Add(new TemplateComposition("d-in-c") { TemplateId = 3, ComposedTemplateId = 4 });
        var a = new Template("A") { Id = 1 };
        a.Compositions.Add(new TemplateComposition("b-in-a") { TemplateId = 1, ComposedTemplateId = 2 });
        a.Compositions.Add(new TemplateComposition("c-in-a") { TemplateId = 1, ComposedTemplateId = 3 });

        StubTemplate(a);
        StubTemplate(b);
        StubTemplate(c);
        StubTemplate(d);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectTemplates(1), CancellationToken.None);

        Assert.Equal(4, result.Templates.Count);
        Assert.Single(result.Templates, t => t.Id == 4);
    }

    [Fact]
    public async Task Resolve_includes_template_folder_for_each_selected_template()
    {
        var root = new TemplateFolder("Root") { Id = 1, ParentFolderId = null };
        var child = new TemplateFolder("Child") { Id = 2, ParentFolderId = 1 };
        var grand = new TemplateFolder("Grand") { Id = 3, ParentFolderId = 2 };
        var t = new Template("T") { Id = 99, FolderId = 3 };

        StubTemplate(t);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders(root, child, grand,
            new TemplateFolder("Unrelated") { Id = 4, ParentFolderId = null });

        var result = await Sut().ResolveAsync(SelectTemplates(99), CancellationToken.None);

        Assert.Equal(3, result.TemplateFolders.Count);
        // Root-first ordering: depth 0, 1, 2.
        Assert.Equal("Root", result.TemplateFolders[0].Name);
        Assert.Equal("Child", result.TemplateFolders[1].Name);
        Assert.Equal("Grand", result.TemplateFolders[2].Name);
    }

    [Fact]
    public async Task Resolve_returns_topological_order_base_before_derived()
    {
        // Top composes Middle, Middle composes Leaf. Order must be Leaf, Middle, Top.
        var leaf = new Template("Leaf") { Id = 30 };
        var middle = new Template("Middle") { Id = 20 };
        middle.Compositions.Add(new TemplateComposition("l") { TemplateId = 20, ComposedTemplateId = 30 });
        var top = new Template("Top") { Id = 10 };
        top.Compositions.Add(new TemplateComposition("m") { TemplateId = 10, ComposedTemplateId = 20 });

        StubTemplate(top);
        StubTemplate(middle);
        StubTemplate(leaf);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectTemplates(10), CancellationToken.None);

        Assert.Equal(3, result.Templates.Count);
        Assert.Equal("Leaf", result.Templates[0].Name);
        Assert.Equal("Middle", result.Templates[1].Name);
        Assert.Equal("Top", result.Templates[2].Name);
    }

    // ---------------------------------------------------------------------------
    // M8 (B1): site/instance-scoped expansion.
    // ---------------------------------------------------------------------------

    [Fact]
    public async Task Resolve_site_pulls_its_connections_and_instances()
    {
        var site = new Site("Plant North", "site-n") { Id = 1 };
        var conn = new DataConnection("opc-1", "OpcUa", site.Id) { Id = 5 };
        var template = new Template("PumpTpl") { Id = 9 };
        var instance = new Instance("Pump01") { Id = 30, SiteId = site.Id, TemplateId = template.Id };

        StubSite(site);
        StubSiteConnections(site.Id, conn);
        StubSiteInstances(site.Id, instance);
        StubInstance(instance);
        StubTemplate(template);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectSites(new[] { site.Id }), CancellationToken.None);

        Assert.Single(result.Sites, s => s.SiteIdentifier == "site-n");
        Assert.Single(result.DataConnections, c => c.Name == "opc-1");
        Assert.Single(result.Instances, i => i.UniqueName == "Pump01");
        // IncludeDependencies feeds the instance's template into the template closure.
        Assert.Single(result.Templates, t => t.Id == 9);
    }

    [Fact]
    public async Task Resolve_instance_with_deps_pulls_site_connections_and_template()
    {
        var site = new Site("Plant South", "site-s") { Id = 2 };
        var boundConn = new DataConnection("opc-bound", "OpcUa", site.Id) { Id = 11 };
        var alarmConn = new DataConnection("opc-alarm", "OpcUa", site.Id) { Id = 12 };
        var unusedConn = new DataConnection("opc-unused", "OpcUa", site.Id) { Id = 13 };
        var template = new Template("TankTpl") { Id = 40 };

        var instance = new Instance("Tank01") { Id = 60, SiteId = site.Id, TemplateId = template.Id };
        instance.ConnectionBindings.Add(new InstanceConnectionBinding("level") { InstanceId = 60, DataConnectionId = boundConn.Id });
        instance.NativeAlarmSourceOverrides.Add(
            new InstanceNativeAlarmSourceOverride("Tank.HiHi") { InstanceId = 60, ConnectionNameOverride = "opc-alarm" });

        StubInstance(instance);
        StubSite(site);
        // The instance's site connections are loaded during dependency expansion.
        StubSiteConnections(site.Id, boundConn, alarmConn, unusedConn);
        StubTemplate(template);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectInstances(new[] { instance.Id }), CancellationToken.None);

        // Owning site pulled in.
        Assert.Single(result.Sites, s => s.SiteIdentifier == "site-s");
        // Template fed into the closure.
        Assert.Single(result.Templates, t => t.Id == 40);
        // Only the connections the instance references travel — bound (by id) + alarm (by name).
        Assert.Contains(result.DataConnections, c => c.Name == "opc-bound");
        Assert.Contains(result.DataConnections, c => c.Name == "opc-alarm");
        Assert.DoesNotContain(result.DataConnections, c => c.Name == "opc-unused");
        // The instance carries its overrides/bindings on its navigation collections.
        var resolvedInstance = Assert.Single(result.Instances);
        Assert.Single(resolvedInstance.ConnectionBindings);
        Assert.Single(resolvedInstance.NativeAlarmSourceOverrides);
    }

    [Fact]
    public async Task Resolve_instance_without_deps_does_not_pull_site_or_template()
    {
        var site = new Site("Plant West", "site-w") { Id = 3 };
        var template = new Template("ValveTpl") { Id = 41 };
        var instance = new Instance("Valve01") { Id = 70, SiteId = site.Id, TemplateId = template.Id };

        StubInstance(instance);
        StubSite(site);
        StubSiteConnections(site.Id);
        StubTemplate(template);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(
            SelectInstances(new[] { instance.Id }, includeDeps: false),
            CancellationToken.None);

        // The instance is present, but with deps off neither its site nor its template are pulled.
        Assert.Single(result.Instances, i => i.UniqueName == "Valve01");
        Assert.Empty(result.Sites);
        Assert.Empty(result.Templates);
    }

    [Fact]
    public async Task Resolve_dedups_instance_when_site_and_instance_both_selected()
    {
        var site = new Site("Plant East", "site-e") { Id = 4 };
        var template = new Template("MixerTpl") { Id = 42 };
        var instance = new Instance("Mixer01") { Id = 80, SiteId = site.Id, TemplateId = template.Id };

        StubSite(site);
        StubSiteConnections(site.Id);
        StubSiteInstances(site.Id, instance);
        StubInstance(instance);
        StubTemplate(template);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        // Select the site AND the instance it owns — the instance must appear once.
        var result = await Sut().ResolveAsync(
            SelectInstances(new[] { instance.Id }, includeDeps: true, siteIds: new[] { site.Id }),
            CancellationToken.None);

        Assert.Single(result.Instances, i => i.UniqueName == "Mixer01");
        Assert.Single(result.Sites, s => s.SiteIdentifier == "site-e");
        // The instance was loaded exactly once (site path), not re-loaded via the instance path.
        await _templates.Received(1).GetInstanceByIdAsync(instance.Id, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Resolve_orders_sites_connections_and_instances_deterministically()
    {
        // Two sites, out of identifier order; each with two connections out of name order;
        // instances out of unique-name order. Result must be sorted.
        var siteB = new Site("Beta", "site-b") { Id = 1 };
        var siteA = new Site("Alpha", "site-a") { Id = 2 };

        var connB2 = new DataConnection("zeta", "OpcUa", siteB.Id) { Id = 10 };
        var connB1 = new DataConnection("alpha", "OpcUa", siteB.Id) { Id = 11 };
        var connA1 = new DataConnection("mike", "OpcUa", siteA.Id) { Id = 12 };

        var instZ = new Instance("Zulu") { Id = 20, SiteId = siteA.Id, TemplateId = 0 };
        var instA = new Instance("Alpha") { Id = 21, SiteId = siteB.Id, TemplateId = 0 };

        StubSite(siteB);
        StubSite(siteA);
        StubSiteConnections(siteB.Id, connB2, connB1);
        StubSiteConnections(siteA.Id, connA1);
        StubSiteInstances(siteB.Id, instA);
        StubSiteInstances(siteA.Id, instZ);
        StubInstance(instZ);
        StubInstance(instA);
        // TemplateId 0 → no template; GetTemplateWithChildrenAsync(0) returns null by default.
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectSites(new[] { siteB.Id, siteA.Id }), CancellationToken.None);

        // Sites by SiteIdentifier.
        Assert.Equal(new[] { "site-a", "site-b" }, result.Sites.Select(s => s.SiteIdentifier).ToArray());
        // Connections by (SiteIdentifier, Name): site-a/mike, site-b/alpha, site-b/zeta.
        Assert.Equal(
            new[] { "mike", "alpha", "zeta" },
            result.DataConnections.Select(c => c.Name).ToArray());
        // Instances by UniqueName.
        Assert.Equal(new[] { "Alpha", "Zulu" }, result.Instances.Select(i => i.UniqueName).ToArray());
    }

    [Fact]
    public async Task Resolve_emits_manifest_entries_with_dependency_edges_for_site_scope()
    {
        var site = new Site("Plant North", "site-n") { Id = 1 };
        var conn = new DataConnection("opc-1", "OpcUa", site.Id) { Id = 5 };
        var template = new Template("PumpTpl") { Id = 9 };
        var instance = new Instance("Pump01") { Id = 30, SiteId = site.Id, TemplateId = template.Id };
        instance.ConnectionBindings.Add(new InstanceConnectionBinding("flow") { InstanceId = 30, DataConnectionId = conn.Id });

        StubSite(site);
        StubSiteConnections(site.Id, conn);
        StubSiteInstances(site.Id, instance);
        StubInstance(instance);
        StubTemplate(template);
        StubAllSharedScripts();
        StubAllExternalSystems();
        StubAllFolders();

        var result = await Sut().ResolveAsync(SelectSites(new[] { site.Id }), CancellationToken.None);

        var siteEntry = Assert.Single(result.ContentManifest, e => e.Type == "Site");
        Assert.Equal("site-n", siteEntry.Name);
        Assert.Empty(siteEntry.DependsOn);

        var connEntry = Assert.Single(result.ContentManifest, e => e.Type == "DataConnection");
        Assert.Equal("site-n/opc-1", connEntry.Name);
        Assert.Contains("Site:site-n", connEntry.DependsOn);

        var instEntry = Assert.Single(result.ContentManifest, e => e.Type == "Instance");
        Assert.Equal("Pump01", instEntry.Name);
        Assert.Contains("Template:PumpTpl", instEntry.DependsOn);
        Assert.Contains("Site:site-n", instEntry.DependsOn);
        Assert.Contains("DataConnection:site-n/opc-1", instEntry.DependsOn);
    }
}
