using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ScadaLink.AuditLog.Site;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Enums;

namespace ScadaLink.AuditLog.Tests.Site;

/// <summary>
/// Bundle B (M2-T2) hot-path tests for <see cref="SqliteAuditWriter"/>. Exercise
/// the Channel-based enqueue, the background writer's batch INSERTs, duplicate-
/// EventId swallowing, ForwardState defaulting, and the
/// <see cref="SqliteAuditWriter.ReadPendingAsync"/> /
/// <see cref="SqliteAuditWriter.MarkForwardedAsync"/> support surface that
/// Bundle D's telemetry actor will call.
/// </summary>
public class SqliteAuditWriterWriteTests
{
    private static (SqliteAuditWriter writer, string dataSource) CreateWriter(
        string testName,
        int? channelCapacity = null)
    {
        var dataSource = $"file:{testName}-{Guid.NewGuid():N}?mode=memory&cache=shared";
        var opts = new SqliteAuditWriterOptions { DatabasePath = dataSource };
        if (channelCapacity is int cap)
        {
            opts.ChannelCapacity = cap;
        }

        var writer = new SqliteAuditWriter(
            Options.Create(opts),
            NullLogger<SqliteAuditWriter>.Instance,
            connectionStringOverride: $"Data Source={dataSource};Cache=Shared");
        return (writer, dataSource);
    }

    private static SqliteConnection OpenVerifierConnection(string dataSource)
    {
        var connection = new SqliteConnection($"Data Source={dataSource};Cache=Shared");
        connection.Open();
        return connection;
    }

    private static AuditEvent NewEvent(Guid? id = null, DateTime? occurredAtUtc = null)
    {
        return new AuditEvent
        {
            EventId = id ?? Guid.NewGuid(),
            OccurredAtUtc = occurredAtUtc ?? DateTime.UtcNow,
            Channel = AuditChannel.ApiOutbound,
            Kind = AuditKind.ApiCall,
            Status = AuditStatus.Delivered,
            PayloadTruncated = false,
        };
    }

    [Fact]
    public async Task WriteAsync_FreshEvent_PersistsWithForwardStatePending()
    {
        var (writer, dataSource) = CreateWriter(nameof(WriteAsync_FreshEvent_PersistsWithForwardStatePending));
        await using var _ = writer;

        var evt = NewEvent();
        await writer.WriteAsync(evt);

        using var connection = OpenVerifierConnection(dataSource);
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT ForwardState FROM AuditLog WHERE EventId = $id;";
        cmd.Parameters.AddWithValue("$id", evt.EventId.ToString());
        var actual = cmd.ExecuteScalar() as string;

        Assert.Equal(AuditForwardState.Pending.ToString(), actual);
    }

    [Fact]
    public async Task WriteAsync_Concurrent_1000Calls_All_Persist_NoExceptions()
    {
        var (writer, dataSource) = CreateWriter(nameof(WriteAsync_Concurrent_1000Calls_All_Persist_NoExceptions));
        await using var _ = writer;

        var events = Enumerable.Range(0, 1000).Select(_ => NewEvent()).ToList();

        await Parallel.ForEachAsync(events, new ParallelOptions { MaxDegreeOfParallelism = 16 },
            async (evt, ct) => await writer.WriteAsync(evt, ct));

        using var connection = OpenVerifierConnection(dataSource);
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM AuditLog;";
        var count = Convert.ToInt64(cmd.ExecuteScalar());

        Assert.Equal(1000, count);
    }

    [Fact]
    public async Task WriteAsync_DuplicateEventId_FirstWriteWins_NoException()
    {
        var (writer, dataSource) = CreateWriter(nameof(WriteAsync_DuplicateEventId_FirstWriteWins_NoException));
        await using var _ = writer;

        var sharedId = Guid.NewGuid();
        var first = NewEvent(sharedId) with { Target = "first" };
        var second = NewEvent(sharedId) with { Target = "second" };

        await writer.WriteAsync(first);
        await writer.WriteAsync(second);

        using var connection = OpenVerifierConnection(dataSource);
        using var countCmd = connection.CreateCommand();
        countCmd.CommandText = "SELECT COUNT(*) FROM AuditLog WHERE EventId = $id;";
        countCmd.Parameters.AddWithValue("$id", sharedId.ToString());
        var count = Convert.ToInt64(countCmd.ExecuteScalar());

        Assert.Equal(1, count);

        using var targetCmd = connection.CreateCommand();
        targetCmd.CommandText = "SELECT Target FROM AuditLog WHERE EventId = $id;";
        targetCmd.Parameters.AddWithValue("$id", sharedId.ToString());
        Assert.Equal("first", targetCmd.ExecuteScalar() as string);
    }

    [Fact]
    public async Task WriteAsync_ForcesForwardStatePending_IfNull()
    {
        var (writer, dataSource) = CreateWriter(nameof(WriteAsync_ForcesForwardStatePending_IfNull));
        await using var _ = writer;

        var evt = NewEvent() with { ForwardState = null };
        await writer.WriteAsync(evt);

        using var connection = OpenVerifierConnection(dataSource);
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT ForwardState FROM AuditLog WHERE EventId = $id;";
        cmd.Parameters.AddWithValue("$id", evt.EventId.ToString());

        Assert.Equal(AuditForwardState.Pending.ToString(), cmd.ExecuteScalar() as string);
    }

    [Fact]
    public async Task ReadPendingAsync_Returns_OldestFirst_LimitedToN()
    {
        var (writer, _) = CreateWriter(nameof(ReadPendingAsync_Returns_OldestFirst_LimitedToN));
        await using var _writer = writer;

        var baseTime = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
        var evts = new[]
        {
            NewEvent(occurredAtUtc: baseTime.AddSeconds(5)),
            NewEvent(occurredAtUtc: baseTime.AddSeconds(1)),
            NewEvent(occurredAtUtc: baseTime.AddSeconds(3)),
            NewEvent(occurredAtUtc: baseTime.AddSeconds(2)),
            NewEvent(occurredAtUtc: baseTime.AddSeconds(4)),
        };

        foreach (var e in evts)
        {
            await writer.WriteAsync(e);
        }

        var rows = await writer.ReadPendingAsync(limit: 3);

        Assert.Equal(3, rows.Count);
        Assert.Equal(baseTime.AddSeconds(1), rows[0].OccurredAtUtc);
        Assert.Equal(baseTime.AddSeconds(2), rows[1].OccurredAtUtc);
        Assert.Equal(baseTime.AddSeconds(3), rows[2].OccurredAtUtc);
    }

    [Fact]
    public async Task MarkForwardedAsync_FlipsRowsToForwarded()
    {
        var (writer, dataSource) = CreateWriter(nameof(MarkForwardedAsync_FlipsRowsToForwarded));
        await using var _ = writer;

        var ids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
        foreach (var id in ids)
        {
            await writer.WriteAsync(NewEvent(id));
        }

        await writer.MarkForwardedAsync(ids);

        using var connection = OpenVerifierConnection(dataSource);
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT ForwardState, COUNT(*) FROM AuditLog GROUP BY ForwardState;";
        using var reader = cmd.ExecuteReader();
        var byState = new Dictionary<string, long>();
        while (reader.Read())
        {
            byState[reader.GetString(0)] = reader.GetInt64(1);
        }

        Assert.Equal(3, byState[AuditForwardState.Forwarded.ToString()]);
        Assert.False(byState.ContainsKey(AuditForwardState.Pending.ToString()));
    }

    [Fact]
    public async Task MarkForwardedAsync_NonExistentId_NoThrow()
    {
        var (writer, _) = CreateWriter(nameof(MarkForwardedAsync_NonExistentId_NoThrow));
        await using var _writer = writer;

        var phantomIds = new[] { Guid.NewGuid(), Guid.NewGuid() };

        await writer.MarkForwardedAsync(phantomIds);
        // No assertion needed: the call must complete without throwing.
    }
}
