using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ScadaLink.Commons.Entities.Deployment;
using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.ConfigurationDatabase;

namespace ScadaLink.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 : ScadaLinkDbContext
{
    public ConcurrencyTestDbContext(DbContextOptions<ScadaLinkDbContext> 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();
        });
    }
}

public class ConcurrencyTests : IDisposable
{
    private readonly string _dbPath;

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

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

    private ScadaLinkDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ScadaLinkDbContext>()
            .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 void DeploymentRecord_HasRowVersionConfigured()
    {
        // Verify the production configuration has a RowVersion shadow property
        var options = new DbContextOptionsBuilder<ScadaLinkDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;

        using var context = new ScadaLinkDbContext(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);
    }
}
