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

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

/// <summary>
///     End-to-end round-trip through the DB for the <see cref="DriverHostStatus"/> entity
///     added in PR 33 — exercises the composite primary key (NodeId, DriverInstanceId,
///     HostName), string-backed <c>DriverHostState</c> conversion, and the two indexes the
///     Admin UI's drill-down queries will scan (NodeId, LastSeenUtc).
/// </summary>
[Trait("Category", "SchemaCompliance")]
[Collection(nameof(SchemaComplianceCollection))]
public sealed class DriverHostStatusTests(SchemaComplianceFixture fixture)
{
    /// <summary>Verifies that the composite key allows the same host across different nodes or drivers.</summary>
    [Fact]
    public async Task Composite_key_allows_same_host_across_different_nodes_or_drivers()
    {
        await using var ctx = NewContext();

        // Same HostName + DriverInstanceId across two different server nodes — classic 2-node
        // redundancy case. Both rows must be insertable because each server node owns its own
        // runtime view of the shared host.
        var now = DateTime.UtcNow;
        ctx.DriverHostStatuses.Add(new DriverHostStatus
        {
            NodeId = "node-a", DriverInstanceId = "galaxy-1", HostName = "GRPlatform",
            State = DriverHostState.Running,
            StateChangedUtc = now, LastSeenUtc = now,
        });
        ctx.DriverHostStatuses.Add(new DriverHostStatus
        {
            NodeId = "node-b", DriverInstanceId = "galaxy-1", HostName = "GRPlatform",
            State = DriverHostState.Stopped,
            StateChangedUtc = now, LastSeenUtc = now,
            Detail = "secondary hasn't taken over yet",
        });
        // Same server node + host, different driver instance — second driver doesn't clobber.
        ctx.DriverHostStatuses.Add(new DriverHostStatus
        {
            NodeId = "node-a", DriverInstanceId = "modbus-plc1", HostName = "GRPlatform",
            State = DriverHostState.Running,
            StateChangedUtc = now, LastSeenUtc = now,
        });
        await ctx.SaveChangesAsync();

        var rows = await ctx.DriverHostStatuses.AsNoTracking()
            .Where(r => r.HostName == "GRPlatform").ToListAsync();

        rows.Count.ShouldBe(3);
        rows.ShouldContain(r => r.NodeId == "node-a" && r.DriverInstanceId == "galaxy-1");
        rows.ShouldContain(r => r.NodeId == "node-b" && r.State == DriverHostState.Stopped && r.Detail == "secondary hasn't taken over yet");
        rows.ShouldContain(r => r.NodeId == "node-a" && r.DriverInstanceId == "modbus-plc1");
    }

    /// <summary>Verifies that the upsert pattern updates existing records in place.</summary>
    [Fact]
    public async Task Upsert_pattern_for_same_key_updates_in_place()
    {
        // The publisher hosted service (follow-up PR) upserts on every transition +
        // heartbeat. This test pins the two-step pattern it will use: check-then-add-or-update
        // keyed on the composite PK. If the composite key ever changes, this test breaks
        // loudly so the publisher gets a synchronized update.
        await using var ctx = NewContext();
        var t0 = DateTime.UtcNow;
        ctx.DriverHostStatuses.Add(new DriverHostStatus
        {
            NodeId = "upsert-node", DriverInstanceId = "upsert-driver", HostName = "upsert-host",
            State = DriverHostState.Running,
            StateChangedUtc = t0, LastSeenUtc = t0,
        });
        await ctx.SaveChangesAsync();

        var t1 = t0.AddSeconds(30);
        await using (var ctx2 = NewContext())
        {
            var existing = await ctx2.DriverHostStatuses.SingleAsync(r =>
                r.NodeId == "upsert-node" && r.DriverInstanceId == "upsert-driver" && r.HostName == "upsert-host");
            existing.State = DriverHostState.Faulted;
            existing.StateChangedUtc = t1;
            existing.LastSeenUtc = t1;
            existing.Detail = "transport reset by peer";
            await ctx2.SaveChangesAsync();
        }

        await using var ctx3 = NewContext();
        var final = await ctx3.DriverHostStatuses.AsNoTracking().SingleAsync(r =>
            r.NodeId == "upsert-node" && r.HostName == "upsert-host");
        final.State.ShouldBe(DriverHostState.Faulted);
        final.Detail.ShouldBe("transport reset by peer");
        // Only one row — a naive "always insert" would have created a duplicate PK and thrown.
        (await ctx3.DriverHostStatuses.CountAsync(r => r.NodeId == "upsert-node")).ShouldBe(1);
    }

    /// <summary>Verifies that the State enum is persisted as a string, not an integer.</summary>
    [Fact]
    public async Task Enum_persists_as_string_not_int()
    {
        // Fluent config sets HasConversion<string>() on State — the DB stores 'Running' /
        // 'Stopped' / 'Faulted' / 'Unknown' as nvarchar(16). Verify by reading the raw
        // string back via ADO; if someone drops the conversion the column will contain '1'
        // / '2' / '3' and this assertion fails. Matters because DBAs inspecting the table
        // directly should see readable state names, not enum ordinals.
        await using var ctx = NewContext();
        ctx.DriverHostStatuses.Add(new DriverHostStatus
        {
            NodeId = "enum-node", DriverInstanceId = "enum-driver", HostName = "enum-host",
            State = DriverHostState.Faulted,
            StateChangedUtc = DateTime.UtcNow, LastSeenUtc = DateTime.UtcNow,
        });
        await ctx.SaveChangesAsync();

        await using var conn = fixture.OpenConnection();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT [State] FROM DriverHostStatus WHERE NodeId = 'enum-node'";
        var rawValue = (string?)await cmd.ExecuteScalarAsync();
        rawValue.ShouldBe("Faulted");
    }

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