using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;

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

public sealed class ConfigComposerTests : ControlPlaneActorTestBase
{
    /// <summary>Verifies that an empty database produces a stable, reproducible hash.</summary>
    [Fact]
    public async Task Empty_database_produces_stable_hash()
    {
        var f = NewInMemoryDbFactory();

        await using var db1 = f.CreateDbContext();
        var a1 = await ConfigComposer.SnapshotAndFlattenAsync(db1);

        await using var db2 = f.CreateDbContext();
        var a2 = await ConfigComposer.SnapshotAndFlattenAsync(db2);

        a1.RevisionHash.ShouldBe(a2.RevisionHash);
        a1.Blob.ShouldBe(a2.Blob);
    }

    /// <summary>Verifies that insertion order does not affect the configuration hash.</summary>
    [Fact]
    public async Task Same_rows_in_different_insert_orders_produce_same_hash()
    {
        var name = Guid.NewGuid().ToString("N");
        var f = NewInMemoryDbFactory(name);

        await using (var db = f.CreateDbContext())
        {
            db.ServerClusters.Add(NewCluster("cluster-a"));
            db.ServerClusters.Add(NewCluster("cluster-b"));
            await db.SaveChangesAsync();
        }
        var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;

        // Fresh DB, same rows in reverse insertion order.
        var f2 = NewInMemoryDbFactory();
        await using (var db = f2.CreateDbContext())
        {
            db.ServerClusters.Add(NewCluster("cluster-b"));
            db.ServerClusters.Add(NewCluster("cluster-a"));
            await db.SaveChangesAsync();
        }
        var hashBA = (await ConfigComposer.SnapshotAndFlattenAsync(f2.CreateDbContext())).RevisionHash;

        hashAB.ShouldBe(hashBA);
    }

    /// <summary>Verifies that different database configurations produce different hashes.</summary>
    [Fact]
    public async Task Different_data_produces_different_hash()
    {
        var f = NewInMemoryDbFactory();
        await using (var db = f.CreateDbContext())
        {
            db.ServerClusters.Add(NewCluster("cluster-a"));
            await db.SaveChangesAsync();
        }
        var hashA = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;

        await using (var db = f.CreateDbContext())
        {
            db.ServerClusters.Add(NewCluster("cluster-b"));
            await db.SaveChangesAsync();
        }
        var hashAB = (await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext())).RevisionHash;

        hashAB.ShouldNotBe(hashA);
    }

    /// <summary>Verifies that the revision hash is a 64-character lowercase hexadecimal string.</summary>
    [Fact]
    public async Task Hash_is_64_lowercase_hex_chars()
    {
        var f = NewInMemoryDbFactory();
        var artifact = await ConfigComposer.SnapshotAndFlattenAsync(f.CreateDbContext());
        artifact.RevisionHash.Length.ShouldBe(64);
        artifact.RevisionHash.ShouldMatch("^[0-9a-f]{64}$");
    }

    private static readonly DateTime FixedTimestamp = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    private static ServerCluster NewCluster(string id) => new()
    {
        ClusterId = id,
        Name = id,
        Enterprise = "ent",
        Site = "site",
        RedundancyMode = RedundancyMode.None,
        CreatedBy = "test",
        // Pin every timestamp so two harnesses produce byte-identical snapshots when the logical
        // content matches. Production rows get real DateTime.UtcNow — divergence there is correct.
        CreatedAt = FixedTimestamp,
    };
}
