using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Server.Dashboard;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;

/// <summary>
///     Unit tests for <see cref="DashboardGalaxySummaryProjector"/>, the host-side
///     projection that turns a shared-library <see cref="GalaxyHierarchyCacheEntry"/>
///     into the dashboard's <see cref="DashboardGalaxySummary"/>.
/// </summary>
public sealed class DashboardGalaxySummaryProjectorTests
{
    /// <summary>
    ///     Verifies that a healthy entry maps status, copies timestamps/counts, and
    ///     groups objects into template usage (ordered by instance count) and
    ///     category counts with resolved names.
    /// </summary>
    [Fact]
    public void Project_HealthyEntry_MapsStatusCopiesCountsAndGroupsObjects()
    {
        DateTimeOffset queriedAt = new(2026, 6, 25, 10, 0, 0, TimeSpan.Zero);
        DateTimeOffset successAt = new(2026, 6, 25, 10, 0, 1, TimeSpan.Zero);
        DateTimeOffset deployAt = new(2026, 6, 24, 8, 30, 0, TimeSpan.Zero);

        // Two templates (PumpTemplate x2, ValveTemplate x1) and two categories
        // (10 = UserDefined x3, 13 = Area x1).
        GalaxyObject area = new()
        {
            GobjectId = 1,
            BrowseName = "AreaA",
            IsArea = true,
            CategoryId = 13,
        };
        GalaxyObject pumpA = new()
        {
            GobjectId = 2,
            BrowseName = "Pump01",
            CategoryId = 10,
        };
        pumpA.TemplateChain.Add("$PumpTemplate");
        GalaxyObject pumpB = new()
        {
            GobjectId = 3,
            BrowseName = "Pump02",
            CategoryId = 10,
        };
        pumpB.TemplateChain.Add("$PumpTemplate");
        GalaxyObject valve = new()
        {
            GobjectId = 4,
            BrowseName = "Valve01",
            CategoryId = 10,
        };
        valve.TemplateChain.Add("$ValveTemplate");

        GalaxyObject[] objects = [area, pumpA, pumpB, valve];

        GalaxyHierarchyCacheEntry entry = new(
            Status: GalaxyCacheStatus.Healthy,
            Sequence: 7,
            LastQueriedAt: queriedAt,
            LastSuccessAt: successAt,
            LastDeployTime: deployAt,
            LastError: null,
            Objects: objects,
            Index: GalaxyHierarchyIndex.Build(objects),
            ObjectCount: 4,
            AreaCount: 1,
            AttributeCount: 12,
            HistorizedAttributeCount: 5,
            AlarmAttributeCount: 2);

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(DashboardGalaxyStatus.Healthy, summary.Status);
        Assert.Equal(queriedAt, summary.LastQueriedAt);
        Assert.Equal(successAt, summary.LastSuccessAt);
        Assert.Equal(deployAt, summary.LastDeployTime);
        Assert.Null(summary.LastError);
        Assert.Equal(4, summary.ObjectCount);
        Assert.Equal(1, summary.AreaCount);
        Assert.Equal(12, summary.AttributeCount);
        Assert.Equal(5, summary.HistorizedAttributeCount);
        Assert.Equal(2, summary.AlarmAttributeCount);

        // Templates ordered by instance count descending: PumpTemplate (2) before ValveTemplate (1).
        Assert.Equal(2, summary.TopTemplates.Count);
        Assert.Equal("$PumpTemplate", summary.TopTemplates[0].TemplateName);
        Assert.Equal(2, summary.TopTemplates[0].InstanceCount);
        Assert.Equal("$ValveTemplate", summary.TopTemplates[1].TemplateName);
        Assert.Equal(1, summary.TopTemplates[1].InstanceCount);

        // Categories ordered by object count descending: UserDefined (3) before Area (1).
        Assert.Equal(2, summary.ObjectCategories.Count);
        Assert.Equal(10, summary.ObjectCategories[0].CategoryId);
        Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName);
        Assert.Equal(3, summary.ObjectCategories[0].ObjectCount);
        Assert.Equal(13, summary.ObjectCategories[1].CategoryId);
        Assert.Equal("Area", summary.ObjectCategories[1].CategoryName);
        Assert.Equal(1, summary.ObjectCategories[1].ObjectCount);
    }

    /// <summary>
    ///     Verifies that an unknown entry with no objects projects to the
    ///     <see cref="DashboardGalaxySummary.Unknown"/>-equivalent summary.
    /// </summary>
    [Fact]
    public void Project_UnknownEmptyEntry_ProducesUnknownEquivalentSummary()
    {
        GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty;

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(DashboardGalaxyStatus.Unknown, summary.Status);
        Assert.Null(summary.LastQueriedAt);
        Assert.Null(summary.LastSuccessAt);
        Assert.Null(summary.LastDeployTime);
        Assert.Null(summary.LastError);
        Assert.Equal(0, summary.ObjectCount);
        Assert.Equal(0, summary.AreaCount);
        Assert.Equal(0, summary.AttributeCount);
        Assert.Equal(0, summary.HistorizedAttributeCount);
        Assert.Equal(0, summary.AlarmAttributeCount);
        Assert.Empty(summary.TopTemplates);
        Assert.Empty(summary.ObjectCategories);
    }

    /// <summary>
    ///     Verifies that the cache status enum is faithfully mapped to the dashboard
    ///     status enum across every defined value.
    /// </summary>
    /// <param name="cacheStatus">The Galaxy cache status to project.</param>
    /// <param name="expected">The expected dashboard status for the given cache status.</param>
    [Theory]
    [InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)]
    [InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)]
    [InlineData(GalaxyCacheStatus.Unavailable, DashboardGalaxyStatus.Unavailable)]
    [InlineData(GalaxyCacheStatus.Unknown, DashboardGalaxyStatus.Unknown)]
    public void Project_MapsCacheStatusToDashboardStatus(
        GalaxyCacheStatus cacheStatus,
        DashboardGalaxyStatus expected)
    {
        GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with { Status = cacheStatus };

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(expected, summary.Status);
    }

    /// <summary>
    ///     Verifies that templates with equal instance counts are ordered
    ///     alphabetically (the <c>ThenBy(name, OrdinalIgnoreCase)</c> tie-break).
    /// </summary>
    [Fact]
    public void Project_TopTemplates_TiesBreakAlphabetically()
    {
        // Two templates, one instance each, intentionally added B before A so the
        // result order can only come from the alphabetical tie-break, not insertion.
        GalaxyObject bObj = MakeObject(1, categoryId: 10, template: "$BTemplate");
        GalaxyObject aObj = MakeObject(2, categoryId: 10, template: "$ATemplate");
        GalaxyObject[] objects = [bObj, aObj];

        GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects);

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(2, summary.TopTemplates.Count);
        Assert.Equal("$ATemplate", summary.TopTemplates[0].TemplateName);
        Assert.Equal(1, summary.TopTemplates[0].InstanceCount);
        Assert.Equal("$BTemplate", summary.TopTemplates[1].TemplateName);
        Assert.Equal(1, summary.TopTemplates[1].InstanceCount);
    }

    /// <summary>
    ///     Verifies that categories with equal object counts are ordered by ascending
    ///     category id (the <c>ThenBy(category.Key)</c> tie-break).
    /// </summary>
    [Fact]
    public void Project_ObjectCategories_TiesBreakByAscendingCategoryId()
    {
        // One object in category 26 and one in category 10 (equal counts), with the
        // higher id added first so the result order comes only from the id tie-break.
        GalaxyObject high = MakeObject(1, categoryId: 26, template: "$T1");
        GalaxyObject low = MakeObject(2, categoryId: 10, template: "$T2");
        GalaxyObject[] objects = [high, low];

        GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects);

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(2, summary.ObjectCategories.Count);
        Assert.Equal(10, summary.ObjectCategories[0].CategoryId);
        Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName);
        Assert.Equal(1, summary.ObjectCategories[0].ObjectCount);
        Assert.Equal(26, summary.ObjectCategories[1].CategoryId);
        Assert.Equal("OPCClient", summary.ObjectCategories[1].CategoryName);
        Assert.Equal(1, summary.ObjectCategories[1].ObjectCount);
    }

    /// <summary>
    ///     Verifies that <see cref="DashboardGalaxySummary.TopTemplates"/> is capped at
    ///     the 10 highest-usage templates, and that the dropped template is the correct
    ///     loser by count-then-alphabetical ordering.
    /// </summary>
    [Fact]
    public void Project_TopTemplates_CapsAtTenHighestByCount()
    {
        // 11 distinct templates. "$Template00" gets the most instances; the rest each
        // get a unique higher-than-baseline count so the ordering is unambiguous,
        // except the two lowest ("$KeepZ" and "$DropA") tie at one instance each.
        // With the count tie, "$DropA" sorts before "$KeepZ" alphabetically, so the
        // Take(10) cap must KEEP "$DropA" and DROP "$KeepZ" (the alphabetical loser
        // among the two tied-lowest) — proving cap order is count-then-name, not
        // insertion.
        List<GalaxyObject> objects = new();
        int nextId = 1;

        // Nine templates with descending, distinct counts 11..3.
        for (int i = 0; i < 9; i++)
        {
            int count = 11 - i; // 11, 10, ... 3
            string template = $"$Template{i:00}";
            for (int instance = 0; instance < count; instance++)
            {
                objects.Add(MakeObject(nextId++, categoryId: 10, template: template));
            }
        }

        // Two templates tied at the lowest count (1 each): "$DropA" sorts before
        // "$KeepZ", so the keeper at slot 10 is "$DropA" and "$KeepZ" is dropped.
        objects.Add(MakeObject(nextId++, categoryId: 10, template: "$DropA"));
        objects.Add(MakeObject(nextId++, categoryId: 10, template: "$KeepZ"));

        GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects.ToArray());

        DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);

        Assert.Equal(10, summary.TopTemplates.Count);

        // The nine distinct-count templates lead, in count order.
        Assert.Equal("$Template00", summary.TopTemplates[0].TemplateName);
        Assert.Equal(11, summary.TopTemplates[0].InstanceCount);
        Assert.Equal("$Template08", summary.TopTemplates[8].TemplateName);
        Assert.Equal(3, summary.TopTemplates[8].InstanceCount);

        // Slot 10 is "$DropA" (the alphabetical winner of the lowest-count tie);
        // "$KeepZ" is the dropped loser.
        Assert.Equal("$DropA", summary.TopTemplates[9].TemplateName);
        Assert.Equal(1, summary.TopTemplates[9].InstanceCount);
        Assert.DoesNotContain(summary.TopTemplates, usage => usage.TemplateName == "$KeepZ");
    }

    /// <summary>
    ///     Verifies that <see cref="DashboardGalaxySummaryProjector.Project"/> rejects a
    ///     null entry with <see cref="ArgumentNullException"/>.
    /// </summary>
    [Fact]
    public void Project_NullEntry_Throws()
    {
        Assert.Throws<ArgumentNullException>(() => DashboardGalaxySummaryProjector.Project(null!));
    }

    private static GalaxyObject MakeObject(int gobjectId, int categoryId, string template)
    {
        GalaxyObject obj = new()
        {
            GobjectId = gobjectId,
            BrowseName = $"Object{gobjectId}",
            CategoryId = categoryId,
        };
        obj.TemplateChain.Add(template);
        return obj;
    }

    private static GalaxyHierarchyCacheEntry MakeHealthyEntry(IReadOnlyList<GalaxyObject> objects)
    {
        return new GalaxyHierarchyCacheEntry(
            Status: GalaxyCacheStatus.Healthy,
            Sequence: 1,
            LastQueriedAt: null,
            LastSuccessAt: null,
            LastDeployTime: null,
            LastError: null,
            Objects: objects,
            Index: GalaxyHierarchyIndex.Build(objects),
            ObjectCount: objects.Count,
            AreaCount: 0,
            AttributeCount: 0,
            HistorizedAttributeCount: 0,
            AlarmAttributeCount: 0);
    }
}
