using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

[Collection("Integration")]
public class ReconciliationTests : IAsyncLifetime
{
    private readonly ApiFixture _fixture;
    private readonly HttpClient _http;

    public ReconciliationTests(ApiFixture fixture)
    {
        _fixture = fixture;
        _http    = fixture.CreateClient();
        _http.AsNurse();
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;

    // -------------------------------------------------------------------------
    // Check 1 — Unacknowledged critical alerts
    // -------------------------------------------------------------------------

    [Fact]
    public async Task Check1_StaleCriticalAlert_CreatesReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);
        var alertId     = await IngestCriticalPotassiumAsync(encounterId);

        using (var scope = _fixture.Services.CreateScope())
        {
            var db    = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var alert = await db.ClinicalAlerts.FindAsync(alertId);
            alert!.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31);
            await db.SaveChangesAsync();
        }

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<UnacknowledgedAlertsCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.Equal(1, count);

        using var verifyScope = _fixture.Services.CreateScope();
        var verifyDb = verifyScope.ServiceProvider.GetRequiredService<AppDbContext>();
        var rec = await verifyDb.ReconciliationAlerts
            .FirstOrDefaultAsync(r => r.CheckType   == UnacknowledgedAlertsCheck.CheckType
                                   && r.EncounterId == encounterId
                                   && r.ResolvedAt  == null);

        Assert.NotNull(rec);
        Assert.Contains(alertId.ToString(), rec!.Details);
    }

    [Fact]
    public async Task Check1_AcknowledgedAlert_DoesNotCreateReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);
        var alertId     = await IngestCriticalPotassiumAsync(encounterId);

        var ackResp = await _http.PostAsJsonAsync(
            $"/api/v1/alerts/{alertId}/acknowledge",
            new AcknowledgeAlertRequest("Reviewed."));
        ackResp.EnsureSuccessStatusCode();

        using (var scope = _fixture.Services.CreateScope())
        {
            var db    = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var alert = await db.ClinicalAlerts.FindAsync(alertId);
            alert!.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31);
            await db.SaveChangesAsync();
        }

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<UnacknowledgedAlertsCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.Equal(0, count);
    }

    [Fact]
    public async Task Check1_SecondRun_DoesNotInsertDuplicate()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);
        await IngestCriticalPotassiumAsync(encounterId);

        using (var scope = _fixture.Services.CreateScope())
        {
            var db    = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var alert = await db.ClinicalAlerts.FirstAsync(a => a.EncounterId == encounterId);
            alert.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31);
            await db.SaveChangesAsync();
        }

        await using (var s = _fixture.Services.CreateAsyncScope())
        {
            var c = s.ServiceProvider.GetRequiredService<UnacknowledgedAlertsCheck>();
            await c.RunAsync(CancellationToken.None);
        }

        int secondCount;
        await using (var s = _fixture.Services.CreateAsyncScope())
        {
            var c = s.ServiceProvider.GetRequiredService<UnacknowledgedAlertsCheck>();
            secondCount = await c.RunAsync(CancellationToken.None);
        }

        Assert.Equal(0, secondCount);
    }

    // -------------------------------------------------------------------------
    // Check 2 — Pending orders without results
    // -------------------------------------------------------------------------

    [Fact]
    public async Task Check2_StalePendingOrder_CreatesReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);

        using (var scope = _fixture.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.Orders.Add(new Order
            {
                Id          = Guid.NewGuid(),
                EncounterId = encounterId,
                OrderType   = OrderType.Lab,
                Description = "Comprehensive metabolic panel",
                OrderedBy   = "Dr. Osei",
                Status      = OrderStatus.Pending,
                OrderedAt   = DateTimeOffset.UtcNow.AddHours(-5),
                ResultedAt  = null,
            });
            await db.SaveChangesAsync();
        }

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<PendingOrdersCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.True(count >= 1);

        using var verifyScope = _fixture.Services.CreateScope();
        var db2 = verifyScope.ServiceProvider.GetRequiredService<AppDbContext>();
        var rec = await db2.ReconciliationAlerts
            .FirstOrDefaultAsync(r => r.CheckType   == PendingOrdersCheck.CheckType
                                   && r.EncounterId == encounterId
                                   && r.ResolvedAt  == null);
        Assert.NotNull(rec);
        Assert.Contains("Comprehensive metabolic panel", rec!.Details);
    }

    [Fact]
    public async Task Check2_ResultedOrder_DoesNotCreateReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);

        using (var scope = _fixture.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.Orders.Add(new Order
            {
                Id          = Guid.NewGuid(),
                EncounterId = encounterId,
                OrderType   = OrderType.Lab,
                Description = "CBC",
                OrderedBy   = "Dr. Osei",
                Status      = OrderStatus.Resulted,
                OrderedAt   = DateTimeOffset.UtcNow.AddHours(-5),
                ResultedAt  = DateTimeOffset.UtcNow.AddHours(-4),
            });
            await db.SaveChangesAsync();
        }

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<PendingOrdersCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.Equal(0, count);
    }

    // -------------------------------------------------------------------------
    // Check 3 — Active inpatient without recent observations
    // -------------------------------------------------------------------------

    [Fact]
    public async Task Check3_ActiveInpatientNoObservations_CreatesReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<DisconnectedMonitorsCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.True(count >= 1);

        using var verifyScope = _fixture.Services.CreateScope();
        var db = verifyScope.ServiceProvider.GetRequiredService<AppDbContext>();
        var rec = await db.ReconciliationAlerts
            .FirstOrDefaultAsync(r => r.CheckType   == DisconnectedMonitorsCheck.CheckType
                                   && r.EncounterId == encounterId
                                   && r.ResolvedAt  == null);
        Assert.NotNull(rec);
        Assert.Contains("no observations ever recorded", rec!.Details);
    }

    [Fact]
    public async Task Check3_ActiveInpatientStaleObservation_CreatesReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);

        var ingestResp = await _http.PostAsJsonAsync(
            $"/api/v1/encounters/{encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 72, "bpm", ObservationSource.Device,
                    DateTimeOffset.UtcNow.AddHours(-3), null),
            }));
        ingestResp.EnsureSuccessStatusCode();

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<DisconnectedMonitorsCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.True(count >= 1);

        using var verifyScope = _fixture.Services.CreateScope();
        var db = verifyScope.ServiceProvider.GetRequiredService<AppDbContext>();
        var rec = await db.ReconciliationAlerts
            .FirstOrDefaultAsync(r => r.CheckType   == DisconnectedMonitorsCheck.CheckType
                                   && r.EncounterId == encounterId
                                   && r.ResolvedAt  == null);
        Assert.NotNull(rec);
    }

    [Fact]
    public async Task Check3_RecentObservation_DoesNotCreateReconciliationAlert()
    {
        await PrepareAsync();

        var patientId   = await CreatePatientAsync();
        var encounterId = await CreateActiveEncounterAsync(patientId);

        var ingestResp = await _http.PostAsJsonAsync(
            $"/api/v1/encounters/{encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 72, "bpm", ObservationSource.Device,
                    DateTimeOffset.UtcNow.AddMinutes(-30), null),
            }));
        ingestResp.EnsureSuccessStatusCode();

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<DisconnectedMonitorsCheck>();
        var count = await check.RunAsync(CancellationToken.None);

        Assert.Equal(0, count);
    }

    [Fact]
    public async Task Check3_OutpatientEncounter_IsIgnored()
    {
        await PrepareAsync();

        var patientId = await CreatePatientAsync();

        var resp = await _http.PostAsJsonAsync(
            $"/api/v1/patients/{patientId}/encounters",
            new OpenEncounterRequest(EncounterType.Outpatient, Department.GeneralMedicine, "Dr. Osei"));
        resp.EnsureSuccessStatusCode();
        var body        = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        var encounterId = body!.RootElement.GetProperty("data").GetProperty("id").GetGuid();

        await using var checkScope = _fixture.Services.CreateAsyncScope();
        var check = checkScope.ServiceProvider.GetRequiredService<DisconnectedMonitorsCheck>();
        await check.RunAsync(CancellationToken.None);

        using var verifyScope = _fixture.Services.CreateScope();
        var db = verifyScope.ServiceProvider.GetRequiredService<AppDbContext>();
        var rec = await db.ReconciliationAlerts
            .FirstOrDefaultAsync(r => r.CheckType   == DisconnectedMonitorsCheck.CheckType
                                   && r.EncounterId == encounterId);
        Assert.Null(rec);
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private async Task PrepareAsync()
    {
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await DbResetHelper.ResetAsync(db);

        db.AlertThresholds.AddRange(
            new AlertThreshold
            {
                Id = Guid.NewGuid(),
                ObservationCode = "POTASSIUM_MEQ_L",
                DisplayName = "Serum Potassium",
                Unit = "mEq/L",
                CriticalLow = 2.5m,
                WarningLow = 3.5m,
                WarningHigh = 5.0m,
                CriticalHigh = 6.5m,
                CreatedAt = DateTimeOffset.UtcNow,
            },
            new AlertThreshold
            {
                Id = Guid.NewGuid(),
                ObservationCode = "HEART_RATE",
                DisplayName = "Heart Rate",
                Unit = "bpm",
                CriticalLow = 30,
                WarningLow = 50,
                WarningHigh = 100,
                CriticalHigh = 150,
                CreatedAt = DateTimeOffset.UtcNow,
            });
        await db.SaveChangesAsync();

        var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
        var cache = redis.GetDatabase(1);
        await cache.StringSetAsync(
            "threshold:POTASSIUM_MEQ_L",
            """{"ObservationCode":"POTASSIUM_MEQ_L","CriticalLow":2.5,"WarningLow":3.5,"WarningHigh":5.0,"CriticalHigh":6.5}""");
        await cache.StringSetAsync(
            "threshold:HEART_RATE",
            """{"ObservationCode":"HEART_RATE","CriticalLow":30,"WarningLow":50,"WarningHigh":100,"CriticalHigh":150}""");
    }

    private async Task<Guid> CreatePatientAsync()
    {
        var resp = await _http.PostAsJsonAsync(
            "/api/v1/patients",
            new RegisterPatientRequest("Reconciliation", "Test", new DateOnly(1970, 6, 1), "Other"));
        resp.EnsureSuccessStatusCode();
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        return body!.RootElement.GetProperty("data").GetProperty("id").GetGuid();
    }

    private async Task<Guid> CreateActiveEncounterAsync(Guid patientId)
    {
        var resp = await _http.PostAsJsonAsync(
            $"/api/v1/patients/{patientId}/encounters",
            new OpenEncounterRequest(EncounterType.Inpatient, Department.Icu, "Dr. Osei"));
        resp.EnsureSuccessStatusCode();
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        return body!.RootElement.GetProperty("data").GetProperty("id").GetGuid();
    }

    private async Task<Guid> IngestCriticalPotassiumAsync(Guid encounterId)
    {
        var ingestResp = await _http.PostAsJsonAsync(
            $"/api/v1/encounters/{encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("POTASSIUM_MEQ_L", 2.1m, "mEq/L", ObservationSource.Device,
                    DateTimeOffset.UtcNow, null),
            }));
        ingestResp.EnsureSuccessStatusCode();

        var body = await ingestResp.Content.ReadFromJsonAsync<JsonDocument>();
        return body!.RootElement.GetProperty("data").GetProperty("alertId").GetGuid();
    }
}
