using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

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

/// <summary>
///     v3 WP1 — exercises the DB-enforceable integrity + sibling-name uniqueness rules of the Raw tree
///     against the real SQL schema (the filtered unique indexes over nullable parent columns and the
///     UnsTagReference (Equipment, Tag) uniqueness). Runs against the shared SchemaComplianceFixture DB;
///     every row uses a GUID-suffixed logical id so tests do not clobber one another.
/// </summary>
[Trait("Category", "SchemaCompliance")]
[Collection(nameof(SchemaComplianceCollection))]
public sealed class RawSchemaIntegrityTests
{
    private readonly SchemaComplianceFixture _fixture;

    /// <summary>Initializes a new instance of the <see cref="RawSchemaIntegrityTests"/> class.</summary>
    /// <param name="fixture">The schema compliance fixture (real SQL DB).</param>
    public RawSchemaIntegrityTests(SchemaComplianceFixture fixture) => _fixture = fixture;

    private OtOpcUaConfigDbContext NewContext()
    {
        var options = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
            .UseSqlServer(_fixture.ConnectionString)
            .Options;
        return new OtOpcUaConfigDbContext(options);
    }

    // Logical ids are nvarchar(64); prefix + '-' + 32 hex stays well under that.
    private static string Id(string prefix) => $"{prefix}-{Guid.NewGuid():N}";

    private string SeedCluster()
    {
        var clusterId = Id("cl");
        using var db = NewContext();
        db.ServerClusters.Add(new ServerCluster
        {
            ClusterId = clusterId,
            Name = Id("Cluster"),
            Enterprise = "zb",
            Site = "dev",
            NodeCount = 1,
            RedundancyMode = RedundancyMode.None,
            CreatedBy = "wp1-test",
        });
        db.SaveChanges();
        return clusterId;
    }

    [Fact]
    public void RawFolder_root_sibling_name_must_be_unique_within_cluster()
    {
        var clusterId = SeedCluster();
        using var db = NewContext();
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" });
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, Name = "plc-area" });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void RawFolder_same_root_name_in_different_cluster_is_allowed()
    {
        var clusterA = SeedCluster();
        var clusterB = SeedCluster();
        using var db = NewContext();
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterA, Name = "shared-name" });
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterB, Name = "shared-name" });

        Should.NotThrow(() => db.SaveChanges());
    }

    [Fact]
    public void RawFolder_nested_sibling_name_must_be_unique_within_parent()
    {
        var clusterId = SeedCluster();
        var parentId = Id("rf-parent");
        using var db = NewContext();
        db.RawFolders.Add(new RawFolder { RawFolderId = parentId, ClusterId = clusterId, Name = "parent" });
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" });
        db.RawFolders.Add(new RawFolder { RawFolderId = Id("rf"), ClusterId = clusterId, ParentRawFolderId = parentId, Name = "child" });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void DriverInstance_cluster_root_sibling_name_must_be_unique()
    {
        var clusterId = SeedCluster();
        using var db = NewContext();
        db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" });
        db.DriverInstances.Add(new DriverInstance { DriverInstanceId = Id("di"), ClusterId = clusterId, Name = "modbus", DriverType = "Modbus", DriverConfig = "{}" });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void Device_sibling_name_must_be_unique_within_driver()
    {
        var driverId = Id("di");
        using var db = NewContext();
        db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" });
        db.Devices.Add(new Device { DeviceId = Id("dev"), DriverInstanceId = driverId, Name = "plc-1", DeviceConfig = "{}" });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void TagGroup_device_root_sibling_name_must_be_unique()
    {
        var deviceId = Id("dev");
        using var db = NewContext();
        db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" });
        db.TagGroups.Add(new TagGroup { TagGroupId = Id("tg"), DeviceId = deviceId, Name = "motors" });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void Tag_device_root_sibling_name_must_be_unique()
    {
        var deviceId = Id("dev");
        using var db = NewContext();
        db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed"));
        db.Tags.Add(NewTag(deviceId, tagGroupId: null, name: "speed"));

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void Tag_grouped_sibling_name_must_be_unique_within_group()
    {
        var deviceId = Id("dev");
        var groupId = Id("tg");
        using var db = NewContext();
        db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed"));
        db.Tags.Add(NewTag(deviceId, tagGroupId: groupId, name: "speed"));

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void Tag_same_name_in_different_groups_is_allowed()
    {
        var deviceId = Id("dev");
        using var db = NewContext();
        db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed"));
        db.Tags.Add(NewTag(deviceId, tagGroupId: Id("tg"), name: "speed"));

        Should.NotThrow(() => db.SaveChanges());
    }

    [Fact]
    public void UnsTagReference_equipment_tag_pair_must_be_unique()
    {
        var equipmentId = Id("eq");
        var tagId = Id("tag");
        using var db = NewContext();
        db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId });
        db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = equipmentId, TagId = tagId });

        Should.Throw<DbUpdateException>(() => db.SaveChanges());
    }

    [Fact]
    public void UnsTagReference_same_tag_different_equipment_is_allowed()
    {
        var tagId = Id("tag");
        using var db = NewContext();
        db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId });
        db.UnsTagReferences.Add(new UnsTagReference { UnsTagReferenceId = Id("utr"), EquipmentId = Id("eq"), TagId = tagId });

        Should.NotThrow(() => db.SaveChanges());
    }

    private static Tag NewTag(string deviceId, string? tagGroupId, string name) => new()
    {
        TagId = Id("tag"),
        DeviceId = deviceId,
        TagGroupId = tagGroupId,
        Name = name,
        DataType = "Float",
        AccessLevel = TagAccessLevel.Read,
        TagConfig = "{}",
    };
}
