using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Apply;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;

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

[Trait("Category", "Unit")]
public sealed class GenerationApplierTests
{
    private static DraftSnapshot SnapshotWith(
        IReadOnlyList<DriverInstance>? drivers = null,
        IReadOnlyList<Equipment>? equipment = null,
        IReadOnlyList<Tag>? tags = null) => new()
    {
        GenerationId = 1, ClusterId = "c",
        DriverInstances = drivers ?? [],
        Equipment = equipment ?? [],
        Tags = tags ?? [],
    };

    private static DriverInstance Driver(string id) =>
        new() { DriverInstanceId = id, ClusterId = "c", NamespaceId = "ns", Name = id, DriverType = "ModbusTcp", DriverConfig = "{}" };

    private static Equipment Eq(string id, Guid uuid) =>
        new() { EquipmentUuid = uuid, EquipmentId = id, DriverInstanceId = "d", UnsLineId = "line-a", Name = id, MachineCode = id };

    private static Tag Tag(string id, string name) =>
        new() { TagId = id, DriverInstanceId = "d", Name = name, FolderPath = "/a", DataType = "Int32", AccessLevel = TagAccessLevel.Read, TagConfig = "{}" };

    [Fact]
    public void Diff_from_empty_to_one_driver_five_equipment_fifty_tags_is_all_Added()
    {
        var uuid = (int i) => Guid.Parse($"00000000-0000-0000-0000-{i:000000000000}");
        var equipment = Enumerable.Range(1, 5).Select(i => Eq($"eq-{i}", uuid(i))).ToList();
        var tags = Enumerable.Range(1, 50).Select(i => Tag($"tag-{i}", $"T{i}")).ToList();

        var diff = GenerationDiffer.Compute(from: null,
            to: SnapshotWith(drivers: [Driver("d-1")], equipment: equipment, tags: tags));

        diff.Drivers.Count.ShouldBe(1);
        diff.Drivers.ShouldAllBe(c => c.Kind == ChangeKind.Added);
        diff.Equipment.Count.ShouldBe(5);
        diff.Equipment.ShouldAllBe(c => c.Kind == ChangeKind.Added);
        diff.Tags.Count.ShouldBe(50);
        diff.Tags.ShouldAllBe(c => c.Kind == ChangeKind.Added);
    }

    [Fact]
    public void Diff_flags_single_tag_name_change_as_Modified_only_for_that_tag()
    {
        var before = SnapshotWith(tags: [Tag("tag-1", "Old"), Tag("tag-2", "Keep")]);
        var after = SnapshotWith(tags: [Tag("tag-1", "New"), Tag("tag-2", "Keep")]);

        var diff = GenerationDiffer.Compute(before, after);

        diff.Tags.Count.ShouldBe(1);
        diff.Tags[0].Kind.ShouldBe(ChangeKind.Modified);
        diff.Tags[0].LogicalId.ShouldBe("tag-1");
    }

    [Fact]
    public void Diff_flags_Removed_equipment_and_its_tags()
    {
        var uuid1 = Guid.NewGuid();
        var before = SnapshotWith(
            equipment: [Eq("eq-1", uuid1), Eq("eq-2", Guid.NewGuid())],
            tags: [Tag("tag-1", "A"), Tag("tag-2", "B")]);
        var after = SnapshotWith(
            equipment: [Eq("eq-2", before.Equipment[1].EquipmentUuid)],
            tags: [Tag("tag-2", "B")]);

        var diff = GenerationDiffer.Compute(before, after);

        diff.Equipment.ShouldContain(c => c.Kind == ChangeKind.Removed && c.LogicalId == "eq-1");
        diff.Tags.ShouldContain(c => c.Kind == ChangeKind.Removed && c.LogicalId == "tag-1");
    }

    [Fact]
    public async Task Apply_dispatches_callbacks_in_dependency_order_and_survives_idempotent_retry()
    {
        var callLog = new List<string>();
        var applier = new GenerationApplier(new ApplyCallbacks
        {
            OnDriver = (c, _) => { callLog.Add($"drv:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
            OnEquipment = (c, _) => { callLog.Add($"eq:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
            OnTag = (c, _) => { callLog.Add($"tag:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
        });

        var to = SnapshotWith(
            drivers: [Driver("d-1")],
            equipment: [Eq("eq-1", Guid.NewGuid())],
            tags: [Tag("tag-1", "A")]);

        var result1 = await applier.ApplyAsync(from: null, to, CancellationToken.None);
        result1.Succeeded.ShouldBeTrue();

        // Driver Added must come before Equipment Added must come before Tag Added
        var drvIdx = callLog.FindIndex(s => s.StartsWith("drv:Added"));
        var eqIdx = callLog.FindIndex(s => s.StartsWith("eq:Added"));
        var tagIdx = callLog.FindIndex(s => s.StartsWith("tag:Added"));
        drvIdx.ShouldBeLessThan(eqIdx);
        eqIdx.ShouldBeLessThan(tagIdx);

        // Idempotent retry: re-applying the same diff must not blow up
        var countBefore = callLog.Count;
        var result2 = await applier.ApplyAsync(from: null, to, CancellationToken.None);
        result2.Succeeded.ShouldBeTrue();
        callLog.Count.ShouldBe(countBefore * 2);
    }

    [Fact]
    public async Task Apply_collects_errors_from_failing_callback_without_aborting()
    {
        var applier = new GenerationApplier(new ApplyCallbacks
        {
            OnTag = (c, _) =>
                c.LogicalId == "tag-bad"
                    ? throw new InvalidOperationException("simulated")
                    : Task.CompletedTask,
        });

        var to = SnapshotWith(tags: [Tag("tag-ok", "A"), Tag("tag-bad", "B")]);
        var result = await applier.ApplyAsync(from: null, to, CancellationToken.None);

        result.Succeeded.ShouldBeFalse();
        result.Errors.ShouldContain(e => e.Contains("tag-bad") && e.Contains("simulated"));
    }
}
