using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;

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

public class AuditServiceTests : IDisposable
{
    private readonly ScadaBridgeDbContext _context;
    private readonly AuditCorrelationContext _correlationContext;
    private readonly AuditService _auditService;

    public AuditServiceTests()
    {
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;

        _context = new ScadaBridgeDbContext(options);
        _context.Database.OpenConnection();
        _context.Database.EnsureCreated();
        _correlationContext = new AuditCorrelationContext();
        _auditService = new AuditService(_context, _correlationContext);
    }

    public void Dispose()
    {
        _context.Database.CloseConnection();
        _context.Dispose();
    }

    [Fact]
    public async Task LogAsync_CreatesAuditEntry_CommittedWithEntityChange()
    {
        // Simulate entity change + audit in same transaction
        var template = new Template("TestTemplate");
        _context.Templates.Add(template);

        await _auditService.LogAsync("admin", "Create", "Template", "1", "TestTemplate",
            new { Name = "TestTemplate" });

        // Single SaveChangesAsync commits both
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.Equal("admin", audit.User);
        Assert.Equal("Create", audit.Action);
        Assert.Equal("Template", audit.EntityType);
        Assert.NotNull(audit.AfterStateJson);

        // Template also committed
        Assert.Single(await _context.Templates.ToListAsync());
    }

    [Fact]
    public async Task LogAsync_Rollback_BothChangeAndAuditRolledBack()
    {
        // Use a separate context to simulate rollback via not calling SaveChanges
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlite(_context.Database.GetDbConnection())
            .Options;

        using var context2 = new ScadaBridgeDbContext(options);
        var auditService2 = new AuditService(context2, new AuditCorrelationContext());

        var template = new Template("RollbackTemplate");
        context2.Templates.Add(template);
        await auditService2.LogAsync("admin", "Create", "Template", "99", "RollbackTemplate",
            new { Name = "RollbackTemplate" });

        // Intentionally do NOT call SaveChangesAsync — simulates rollback
        // Verify nothing persisted
        Assert.Empty(await _context.AuditLogEntries.Where(a => a.EntityName == "RollbackTemplate").ToListAsync());
        Assert.Empty(await _context.Templates.Where(t => t.Name == "RollbackTemplate").ToListAsync());
    }

    [Fact]
    public async Task LogAsync_SerializesAfterStateAsJson()
    {
        var state = new { Name = "Test", Value = 42, Nested = new { Prop = "inner" } };
        await _auditService.LogAsync("admin", "Create", "Entity", "1", "Test", state);
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.NotNull(audit.AfterStateJson);

        var deserialized = JsonSerializer.Deserialize<JsonElement>(audit.AfterStateJson!);
        Assert.Equal("Test", deserialized.GetProperty("Name").GetString());
        Assert.Equal(42, deserialized.GetProperty("Value").GetInt32());
    }

    [Fact]
    public async Task LogAsync_NullAfterState_ForDeletes()
    {
        await _auditService.LogAsync("admin", "Delete", "Template", "1", "DeletedTemplate", null);
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.Null(audit.AfterStateJson);
        Assert.Equal("Delete", audit.Action);
    }

    [Fact]
    public async Task LogAsync_SetsTimestampToUtcNow()
    {
        var before = DateTimeOffset.UtcNow;
        await _auditService.LogAsync("admin", "Create", "Template", "1", "T1", new { });
        await _context.SaveChangesAsync();
        var after = DateTimeOffset.UtcNow;

        var audit = await _context.AuditLogEntries.SingleAsync();
        // Allow 2 second tolerance for SQLite precision
        Assert.True(audit.Timestamp >= before.AddSeconds(-2));
        Assert.True(audit.Timestamp <= after.AddSeconds(2));
    }

    [Fact]
    public void AuditService_IsAppendOnly_NoUpdateOrDeleteMethods()
    {
        // Verify IAuditService only exposes LogAsync — no update/delete
        var methods = typeof(IAuditService).GetMethods();
        Assert.Single(methods, m => m.Name == "LogAsync");
        Assert.DoesNotContain(methods, m => m.Name.Contains("Update", StringComparison.OrdinalIgnoreCase));
        Assert.DoesNotContain(methods, m => m.Name.Contains("Delete", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public async Task LogAsync_StampsBundleImportId_FromCorrelationContext()
    {
        // Bundle importer sets the correlation context for the duration of ApplyAsync;
        // every AuditLogEntry written under that scope must carry the BundleImportId so
        // the imported configuration is attributable to the import session.
        var bundleId = Guid.NewGuid();
        _correlationContext.BundleImportId = bundleId;

        await _auditService.LogAsync("admin", "Create", "Template", "1", "BundleImportedTemplate",
            new { Name = "BundleImportedTemplate" });
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.Equal(bundleId, audit.BundleImportId);
    }

    [Fact]
    public async Task LogAsync_LeavesBundleImportIdNull_WhenCorrelationContextHasNoValue()
    {
        // Default code path (interactive user edit, not a bundle import) must leave
        // the column NULL so normal audit rows are distinguishable from bundle-import
        // rows in queries and reports.
        Assert.Null(_correlationContext.BundleImportId);

        await _auditService.LogAsync("admin", "Create", "Template", "1", "InteractiveTemplate",
            new { Name = "InteractiveTemplate" });
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.Null(audit.BundleImportId);
    }

    // Self-referential POCO used to reproduce a reference cycle in afterState.
    private sealed class CyclicNode
    {
        public string Name { get; set; } = "node";
        public CyclicNode? Self { get; set; }
    }

    [Fact]
    public async Task LogAsync_AfterStateWithReferenceCycle_DoesNotThrow_AndDoesNotRollBackOperation()
    {
        // Regression guard for ConfigurationDatabase-007: serializing an afterState
        // object that contains a reference cycle must not throw a JsonException —
        // that would roll back the entire business operation it is auditing.
        var node = new CyclicNode();
        node.Self = node; // reference cycle

        var template = new Template("CyclicAuditTemplate");
        _context.Templates.Add(template);

        // Must not throw.
        await _auditService.LogAsync("admin", "Create", "Template", "1", "CyclicAuditTemplate", node);

        // The audited business operation must still commit successfully.
        await _context.SaveChangesAsync();

        var audit = await _context.AuditLogEntries.SingleAsync();
        Assert.NotNull(audit.AfterStateJson);
        Assert.Contains("node", audit.AfterStateJson);
        Assert.Single(await _context.Templates.Where(t => t.Name == "CyclicAuditTemplate").ToListAsync());
    }
}
