using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace ScadaLink.SiteEventLogging.Tests;

public class EventLogPurgeServiceTests : IDisposable
{
    private readonly SiteEventLogger _eventLogger;
    private readonly string _dbPath;
    private readonly SiteEventLogOptions _options;

    public EventLogPurgeServiceTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"test_purge_{Guid.NewGuid()}.db");
        _options = new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            RetentionDays = 30,
            MaxStorageMb = 1024
        };
        _eventLogger = new SiteEventLogger(
            Options.Create(_options),
            NullLogger<SiteEventLogger>.Instance);
    }

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

    private EventLogPurgeService CreatePurgeService(SiteEventLogOptions? optionsOverride = null)
    {
        var opts = optionsOverride ?? _options;
        return new EventLogPurgeService(
            _eventLogger,
            Options.Create(opts),
            NullLogger<EventLogPurgeService>.Instance);
    }

    private void InsertEventWithTimestamp(DateTimeOffset timestamp)
    {
        _eventLogger.WithConnection(connection =>
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = """
                INSERT INTO site_events (timestamp, event_type, severity, source, message)
                VALUES ($ts, 'script', 'Info', 'Test', 'Test message')
                """;
            cmd.Parameters.AddWithValue("$ts", timestamp.ToString("o"));
            cmd.ExecuteNonQuery();
        });
    }

    private long GetEventCount()
    {
        return _eventLogger.WithConnection(connection =>
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = "SELECT COUNT(*) FROM site_events";
            return (long)cmd.ExecuteScalar()!;
        });
    }

    [Fact]
    public void PurgeByRetention_DeletesOldEvents()
    {
        // Insert an old event (31 days ago) and a recent one
        InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-31));
        InsertEventWithTimestamp(DateTimeOffset.UtcNow);

        var purge = CreatePurgeService();
        purge.RunPurge();

        Assert.Equal(1, GetEventCount());
    }

    [Fact]
    public void PurgeByRetention_KeepsRecentEvents()
    {
        InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-29));
        InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-1));
        InsertEventWithTimestamp(DateTimeOffset.UtcNow);

        var purge = CreatePurgeService();
        purge.RunPurge();

        Assert.Equal(3, GetEventCount());
    }

    [Fact]
    public void PurgeByStorageCap_DeletesOldestWhenOverCap()
    {
        // Insert enough events to have some data
        for (int i = 0; i < 100; i++)
        {
            InsertEventWithTimestamp(DateTimeOffset.UtcNow);
        }

        // Set an artificially small cap to trigger purge
        var smallCapOptions = new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            RetentionDays = 30,
            MaxStorageMb = 0  // 0 MB cap forces purge
        };

        var purge = CreatePurgeService(smallCapOptions);
        purge.RunPurge();

        // All events should be purged since cap is 0
        Assert.Equal(0, GetEventCount());
    }

    [Fact]
    public void GetDatabaseSizeBytes_ReturnsPositiveValue()
    {
        InsertEventWithTimestamp(DateTimeOffset.UtcNow);

        var purge = CreatePurgeService();
        var size = purge.GetDatabaseSizeBytes();

        Assert.True(size > 0);
    }

    private void InsertBulkEvents(int count)
    {
        // Each event carries a sizeable details payload so the database grows
        // measurably and the storage cap can be exercised against a realistic file.
        var details = new string('x', 2000);
        _eventLogger.WithConnection(connection =>
        {
            for (int i = 0; i < count; i++)
            {
                using var cmd = connection.CreateCommand();
                cmd.CommandText = """
                    INSERT INTO site_events (timestamp, event_type, severity, source, message, details)
                    VALUES ($ts, 'script', 'Info', 'Test', 'Bulk event', $details)
                    """;
                cmd.Parameters.AddWithValue("$ts", DateTimeOffset.UtcNow.ToString("o"));
                cmd.Parameters.AddWithValue("$details", details);
                cmd.ExecuteNonQuery();
            }
        });
    }

    private long MinEventId()
    {
        return _eventLogger.WithConnection(connection =>
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = "SELECT MIN(id) FROM site_events";
            var result = cmd.ExecuteScalar();
            return result is long l ? l : 0;
        });
    }

    [Fact]
    public void PurgeByStorageCap_StopsWhenUnderCap_DoesNotEmptyTable()
    {
        // Regression test for SiteEventLogging-001 / -002:
        // a realistic non-zero cap must trim the oldest events to the budget,
        // not delete the entire table.
        InsertBulkEvents(3000);

        var purge = CreatePurgeService();
        var totalSize = purge.GetDatabaseSizeBytes();

        // Cap at roughly half the current database size — purge must keep some rows.
        var capBytes = totalSize / 2;
        var capOptions = new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            RetentionDays = 30,
            MaxStorageMb = (int)Math.Max(1, capBytes / (1024 * 1024))
        };

        var cappedPurge = CreatePurgeService(capOptions);
        cappedPurge.RunPurge();

        var remaining = GetEventCount();
        Assert.True(remaining > 0, "Storage-cap purge must not delete the entire table.");
        Assert.True(remaining < 3000, "Storage-cap purge must remove some events when over cap.");

        // The database must actually be back under the cap after purge.
        var finalSize = cappedPurge.GetDatabaseSizeBytes();
        var finalCapBytes = (long)capOptions.MaxStorageMb * 1024 * 1024;
        Assert.True(finalSize <= finalCapBytes,
            $"Database size {finalSize} must be at or below cap {finalCapBytes} after purge.");
    }

    [Fact]
    public void PurgeByStorageCap_RemovesOldestEventsFirst()
    {
        // Regression test for SiteEventLogging-002: only the oldest events
        // (lowest ids) should be removed when trimming to the cap.
        InsertBulkEvents(3000);

        var purge = CreatePurgeService();
        var totalSize = purge.GetDatabaseSizeBytes();

        var capOptions = new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            RetentionDays = 30,
            MaxStorageMb = (int)Math.Max(1, (totalSize / 2) / (1024 * 1024))
        };

        var minIdBefore = MinEventId();
        var cappedPurge = CreatePurgeService(capOptions);
        cappedPurge.RunPurge();
        var minIdAfter = MinEventId();

        // The surviving rows must be the newest ones — minimum id has advanced.
        Assert.True(minIdAfter > minIdBefore,
            "Oldest events (lowest ids) must be purged first.");

        // The newest event (highest id) must still be present.
        var newestPresent = _eventLogger.WithConnection(connection =>
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = "SELECT COUNT(*) FROM site_events WHERE id = 3000";
            return (long)cmd.ExecuteScalar()!;
        });
        Assert.Equal(1L, newestPresent);
    }

    [Fact]
    public async Task PurgeByStorageCap_ConcurrentWritesDoNotCorruptConnection()
    {
        // Regression test for SiteEventLogging-003: purge running on a background
        // thread while events are recorded on other threads must not throw
        // "DataReader already open" / "connection busy" from a shared connection.
        InsertBulkEvents(2000);

        var purge = CreatePurgeService();
        var totalSize = purge.GetDatabaseSizeBytes();
        var capOptions = new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            RetentionDays = 30,
            MaxStorageMb = (int)Math.Max(1, (totalSize / 2) / (1024 * 1024))
        };

        var exceptions = new System.Collections.Concurrent.ConcurrentBag<Exception>();
        var stop = false;

        var purgeTask = Task.Run(() =>
        {
            try
            {
                var p = CreatePurgeService(capOptions);
                for (int i = 0; i < 20; i++) p.RunPurge();
            }
            catch (Exception ex) { exceptions.Add(ex); }
        });

        var writeTasks = Enumerable.Range(0, 4).Select(_ => Task.Run(async () =>
        {
            try
            {
                while (!stop)
                {
                    await _eventLogger.LogEventAsync("script", "Info", null, "Concurrent", "Concurrent write");
                }
            }
            catch (Exception ex) { exceptions.Add(ex); }
        })).ToArray();

        await purgeTask;
        stop = true;
        await Task.WhenAll(writeTasks);

        Assert.Empty(exceptions);
    }
}
