using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Messages.RemoteQuery;

namespace ScadaLink.SiteEventLogging.Tests;

public class EventLogQueryServiceTests : IDisposable
{
    private readonly SiteEventLogger _eventLogger;
    private readonly EventLogQueryService _queryService;
    private readonly string _dbPath;

    public EventLogQueryServiceTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"test_query_{Guid.NewGuid()}.db");
        var options = Options.Create(new SiteEventLogOptions
        {
            DatabasePath = _dbPath,
            QueryPageSize = 500
        });
        _eventLogger = new SiteEventLogger(options, NullLogger<SiteEventLogger>.Instance);
        _queryService = new EventLogQueryService(
            _eventLogger,
            options,
            NullLogger<EventLogQueryService>.Instance);
    }

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

    private async Task SeedEvents()
    {
        await _eventLogger.LogEventAsync("script", "Error", "inst-1", "ScriptActor:Monitor", "Script timeout");
        await _eventLogger.LogEventAsync("alarm", "Warning", "inst-1", "AlarmActor:TempHigh", "Alarm triggered");
        await _eventLogger.LogEventAsync("deployment", "Info", "inst-2", "DeploymentManager", "Instance deployed");
        await _eventLogger.LogEventAsync("connection", "Error", null, "DCL:OPC1", "Connection lost");
        await _eventLogger.LogEventAsync("script", "Info", "inst-2", "ScriptActor:Calculate", "Script completed");
    }

    private EventLogQueryRequest MakeRequest(
        string? eventType = null,
        string? severity = null,
        string? instanceId = null,
        string? keyword = null,
        long? continuationToken = null,
        int pageSize = 500,
        DateTimeOffset? from = null,
        DateTimeOffset? to = null) =>
        new(
            CorrelationId: Guid.NewGuid().ToString(),
            SiteId: "site-1",
            From: from,
            To: to,
            EventType: eventType,
            Severity: severity,
            InstanceId: instanceId,
            KeywordFilter: keyword,
            ContinuationToken: continuationToken,
            PageSize: pageSize,
            Timestamp: DateTimeOffset.UtcNow);

    [Fact]
    public async Task Query_ReturnsAllEvents_WhenNoFilters()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest());

        Assert.True(response.Success);
        Assert.Equal(5, response.Entries.Count);
        Assert.False(response.HasMore);
    }

    [Fact]
    public async Task Query_FiltersByEventType()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(eventType: "script"));

        Assert.True(response.Success);
        Assert.Equal(2, response.Entries.Count);
        Assert.All(response.Entries, e => Assert.Equal("script", e.EventType));
    }

    [Fact]
    public async Task Query_FiltersBySeverity()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(severity: "Error"));

        Assert.True(response.Success);
        Assert.Equal(2, response.Entries.Count);
        Assert.All(response.Entries, e => Assert.Equal("Error", e.Severity));
    }

    [Fact]
    public async Task Query_FiltersByInstanceId()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(instanceId: "inst-1"));

        Assert.True(response.Success);
        Assert.Equal(2, response.Entries.Count);
        Assert.All(response.Entries, e => Assert.Equal("inst-1", e.InstanceId));
    }

    [Fact]
    public async Task Query_KeywordSearch_MatchesMessage()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(keyword: "timeout"));

        Assert.True(response.Success);
        Assert.Single(response.Entries);
        Assert.Contains("timeout", response.Entries[0].Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Query_KeywordSearch_MatchesSource()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(keyword: "AlarmActor"));

        Assert.True(response.Success);
        Assert.Single(response.Entries);
        Assert.Contains("AlarmActor", response.Entries[0].Source);
    }

    [Fact]
    public async Task Query_CombinesMultipleFilters()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(
            eventType: "script",
            severity: "Error",
            instanceId: "inst-1"));

        Assert.True(response.Success);
        Assert.Single(response.Entries);
        Assert.Equal("Script timeout", response.Entries[0].Message);
    }

    [Fact]
    public async Task Query_Pagination_ReturnsCorrectPageSize()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(pageSize: 2));

        Assert.True(response.Success);
        Assert.Equal(2, response.Entries.Count);
        Assert.True(response.HasMore);
        Assert.NotNull(response.ContinuationToken);
    }

    [Fact]
    public async Task Query_Pagination_ContinuationTokenWorksCorrectly()
    {
        await SeedEvents();

        // Get first page
        var page1 = _queryService.ExecuteQuery(MakeRequest(pageSize: 2));
        Assert.Equal(2, page1.Entries.Count);
        Assert.True(page1.HasMore);

        // Get second page using continuation token
        var page2 = _queryService.ExecuteQuery(MakeRequest(
            pageSize: 2,
            continuationToken: page1.ContinuationToken));
        Assert.Equal(2, page2.Entries.Count);
        Assert.True(page2.HasMore);

        // Get third page
        var page3 = _queryService.ExecuteQuery(MakeRequest(
            pageSize: 2,
            continuationToken: page2.ContinuationToken));
        Assert.Single(page3.Entries);
        Assert.False(page3.HasMore);

        // Verify no overlapping entries
        var allIds = page1.Entries.Select(e => e.Id)
            .Concat(page2.Entries.Select(e => e.Id))
            .Concat(page3.Entries.Select(e => e.Id))
            .ToList();
        Assert.Equal(5, allIds.Distinct().Count());
    }

    [Fact]
    public async Task Query_FiltersByTimeRange()
    {
        // Insert events at controlled times
        var now = DateTimeOffset.UtcNow;

        // Insert with a direct SQL to control timestamps
        InsertEventAt(now.AddHours(-2), "script", "Info", null, "S1", "Old event");
        InsertEventAt(now.AddMinutes(-30), "script", "Info", null, "S2", "Recent event");
        InsertEventAt(now, "script", "Info", null, "S3", "Now event");

        var response = _queryService.ExecuteQuery(MakeRequest(
            from: now.AddHours(-1),
            to: now.AddMinutes(1)));

        Assert.True(response.Success);
        Assert.Equal(2, response.Entries.Count);
    }

    [Fact]
    public async Task Query_EmptyResult_WhenNoMatches()
    {
        await SeedEvents();

        var response = _queryService.ExecuteQuery(MakeRequest(eventType: "nonexistent"));

        Assert.True(response.Success);
        Assert.Empty(response.Entries);
        Assert.False(response.HasMore);
        Assert.Null(response.ContinuationToken);
    }

    [Fact]
    public void Query_ReturnsCorrelationId()
    {
        var request = MakeRequest();
        var response = _queryService.ExecuteQuery(request);

        Assert.Equal(request.CorrelationId, response.CorrelationId);
        Assert.Equal("site-1", response.SiteId);
    }

    [Fact]
    public async Task Query_ReturnsAllEventLogEntryFields()
    {
        await _eventLogger.LogEventAsync("script", "Error", "inst-1", "ScriptActor:Run", "Failure", "{\"stack\":\"trace\"}");

        var response = _queryService.ExecuteQuery(MakeRequest());
        Assert.Single(response.Entries);

        var entry = response.Entries[0];
        Assert.True(entry.Id > 0);
        Assert.Equal("script", entry.EventType);
        Assert.Equal("Error", entry.Severity);
        Assert.Equal("inst-1", entry.InstanceId);
        Assert.Equal("ScriptActor:Run", entry.Source);
        Assert.Equal("Failure", entry.Message);
        Assert.Equal("{\"stack\":\"trace\"}", entry.Details);
    }

    private void InsertEventAt(DateTimeOffset timestamp, string eventType, string severity, string? instanceId, string source, string message)
    {
        _eventLogger.WithConnection(connection =>
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = """
                INSERT INTO site_events (timestamp, event_type, severity, instance_id, source, message)
                VALUES ($ts, $et, $sev, $iid, $src, $msg)
                """;
            cmd.Parameters.AddWithValue("$ts", timestamp.ToString("o"));
            cmd.Parameters.AddWithValue("$et", eventType);
            cmd.Parameters.AddWithValue("$sev", severity);
            cmd.Parameters.AddWithValue("$iid", (object?)instanceId ?? DBNull.Value);
            cmd.Parameters.AddWithValue("$src", source);
            cmd.Parameters.AddWithValue("$msg", message);
            cmd.ExecuteNonQuery();
        });
    }
}
