using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;

/// <summary>
/// A test-specific DbContext that uses an explicit ConcurrencyToken on DeploymentRecord
/// (as opposed to SQL Server's IsRowVersion()) so that SQLite can enforce concurrency.
/// In production, the SQL Server RowVersion provides this automatically.
/// </summary>
public class ConcurrencyTestDbContext : ScadaBridgeDbContext
{
    public ConcurrencyTestDbContext(DbContextOptions<ScadaBridgeDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Replace the SQL Server RowVersion with an explicit concurrency token for SQLite.
        // SQLite can't auto-generate rowversion, so disable it and use Status as the token instead.
        modelBuilder.Entity<DeploymentRecord>(builder =>
        {
            builder.Property(d => d.RowVersion)
                .IsRequired(false)
                .IsConcurrencyToken(false)
                .ValueGeneratedNever();

            builder.Property(d => d.Status).IsConcurrencyToken();
        });
    }
}

/// <summary>
/// A SQLite-friendly DbContext that keeps <see cref="DeploymentRecord.RowVersion"/> as
/// the optimistic-concurrency token but disables auto-generation (SQLite cannot
/// auto-populate a rowversion column). The caller sets RowVersion explicitly, which
/// is sufficient to exercise the production stub-attach delete path under CD-017's
/// concurrency rule.
/// </summary>
public class RowVersionConcurrencyTestDbContext : ScadaBridgeDbContext
{
    public RowVersionConcurrencyTestDbContext(DbContextOptions<ScadaBridgeDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<DeploymentRecord>(builder =>
        {
            builder.Property(d => d.RowVersion)
                .IsRequired(false)
                .IsConcurrencyToken()
                .ValueGeneratedNever();
        });
    }
}

public class ConcurrencyTests : IDisposable
{
    private readonly string _dbPath;

    public ConcurrencyTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_test_{Guid.NewGuid()}.db");
    }

    public void Dispose()
    {
        if (File.Exists(_dbPath))
            File.Delete(_dbPath);
    }

    private ScadaBridgeDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite($"DataSource={_dbPath}")
            .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
            .Options;
        return new ConcurrencyTestDbContext(options);
    }

    [Fact]
    public async Task DeploymentRecord_OptimisticConcurrency_SecondUpdateThrows()
    {
        // Setup: create necessary entities
        using (var setupCtx = CreateContext())
        {
            await setupCtx.Database.EnsureCreatedAsync();

            var site = new Site("Site1", "S-001");
            var template = new Template("T1");
            setupCtx.Sites.Add(site);
            setupCtx.Templates.Add(template);
            await setupCtx.SaveChangesAsync();

            var instance = new Instance("I1")
            {
                SiteId = site.Id,
                TemplateId = template.Id,
                State = InstanceState.Enabled
            };
            setupCtx.Instances.Add(instance);
            await setupCtx.SaveChangesAsync();

            var record = new DeploymentRecord("deploy-concurrent", "admin")
            {
                InstanceId = instance.Id,
                Status = DeploymentStatus.Pending,
                DeployedAt = DateTimeOffset.UtcNow
            };
            setupCtx.DeploymentRecords.Add(record);
            await setupCtx.SaveChangesAsync();
        }

        // Load the same record in two separate contexts
        using var ctx1 = CreateContext();
        using var ctx2 = CreateContext();

        var record1 = await ctx1.DeploymentRecords.SingleAsync(d => d.DeploymentId == "deploy-concurrent");
        var record2 = await ctx2.DeploymentRecords.SingleAsync(d => d.DeploymentId == "deploy-concurrent");

        // Both loaded Status = Pending. First context updates and saves successfully.
        record1.Status = DeploymentStatus.Success;
        record1.CompletedAt = DateTimeOffset.UtcNow;
        await ctx1.SaveChangesAsync();

        // Second context tries to update the same record from the stale "Pending" state — should throw
        // because the Status concurrency token has changed from Pending to Success
        record2.Status = DeploymentStatus.Failed;
        await Assert.ThrowsAsync<DbUpdateConcurrencyException>(
            () => ctx2.SaveChangesAsync());
    }

    [Fact]
    public async Task Template_NoOptimisticConcurrency_LastWriteWins()
    {
        // Setup
        using (var setupCtx = CreateContext())
        {
            await setupCtx.Database.EnsureCreatedAsync();

            var template = new Template("ConcurrentTemplate")
            {
                Description = "Original"
            };
            setupCtx.Templates.Add(template);
            await setupCtx.SaveChangesAsync();
        }

        // Load in two contexts
        using var ctx1 = CreateContext();
        using var ctx2 = CreateContext();

        var template1 = await ctx1.Templates.SingleAsync(t => t.Name == "ConcurrentTemplate");
        var template2 = await ctx2.Templates.SingleAsync(t => t.Name == "ConcurrentTemplate");

        // First update
        template1.Description = "First update";
        await ctx1.SaveChangesAsync();

        // Second update — should succeed (last-write-wins, no concurrency token)
        template2.Description = "Second update";
        await ctx2.SaveChangesAsync(); // Should NOT throw

        // Verify last write won
        using var verifyCtx = CreateContext();
        var loaded = await verifyCtx.Templates.SingleAsync(t => t.Name == "ConcurrentTemplate");
        Assert.Equal("Second update", loaded.Description);
    }

    [Fact]
    public async Task DeleteDeploymentRecord_StaleRowVersion_ThrowsConcurrencyException()
    {
        // CD-017: Verifies the stub-attach delete path enforces optimistic concurrency
        // when the caller passes a RowVersion that no longer matches the row's current
        // RowVersion. Uses a SQLite fixture where DeploymentRecord.RowVersion is an
        // explicit, caller-managed concurrency token (no SQL Server auto-generation).
        using var setupCtx = new RowVersionConcurrencyTestDbContext(BuildOptions());
        await setupCtx.Database.EnsureCreatedAsync();

        var site = new Site("Site1", "S-RV1");
        var template = new Template("RV-T1");
        setupCtx.Sites.Add(site);
        setupCtx.Templates.Add(template);
        await setupCtx.SaveChangesAsync();

        var instance = new Instance("RV-I1") { SiteId = site.Id, TemplateId = template.Id, State = InstanceState.Enabled };
        setupCtx.Instances.Add(instance);
        await setupCtx.SaveChangesAsync();

        var record = new DeploymentRecord("deploy-rv-stale", "admin")
        {
            InstanceId = instance.Id,
            DeployedAt = DateTimeOffset.UtcNow,
            RowVersion = new byte[] { 0x01 },
        };
        setupCtx.DeploymentRecords.Add(record);
        await setupCtx.SaveChangesAsync();
        var id = record.Id;

        // Reload in a fresh context and simulate a concurrent edit that has advanced
        // the stored RowVersion. The caller below holds the *prior* RowVersion (0x01)
        // and is expected to lose the concurrency check.
        using (var advanceCtx = new RowVersionConcurrencyTestDbContext(BuildOptions()))
        {
            var stored = await advanceCtx.DeploymentRecords.SingleAsync(d => d.Id == id);
            stored.RowVersion = new byte[] { 0x02 };
            await advanceCtx.SaveChangesAsync();
        }

        using var deleteCtx = new RowVersionConcurrencyTestDbContext(BuildOptions());
        var repository = new DeploymentManagerRepository(deleteCtx);
        var staleRowVersion = new byte[] { 0x01 };
        await repository.DeleteDeploymentRecordAsync(id, staleRowVersion);

        await Assert.ThrowsAsync<DbUpdateConcurrencyException>(
            () => repository.SaveChangesAsync());
    }

    private DbContextOptions<ScadaBridgeDbContext> BuildOptions()
    {
        return new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite($"DataSource={_dbPath}")
            .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
            .Options;
    }

    [Fact]
    public void DeploymentRecord_HasRowVersionConfigured()
    {
        // Verify the production configuration has a RowVersion shadow property
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;

        using var context = new ScadaBridgeDbContext(options);
        context.Database.OpenConnection();
        context.Database.EnsureCreated();

        var entityType = context.Model.FindEntityType(typeof(DeploymentRecord))!;
        var rowVersion = entityType.FindProperty("RowVersion");
        Assert.NotNull(rowVersion);
        Assert.True(rowVersion!.IsConcurrencyToken);
    }
}
