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

[Collection("Integration")]
public class ObservationIngestTests : IAsyncLifetime
{
    private readonly ApiFixture _fixture;
    private readonly HttpClient _client;
    private Guid _patientId;
    private Guid _encounterId;

    public ObservationIngestTests(ApiFixture fixture)
    {
        _fixture = fixture;
        _client = fixture.CreateClient();
    }

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

        // Seed one patient, one active encounter, and the four thresholds
        var patient = new Patient
        {
            Id = Guid.NewGuid(), Mrn = "MRN-T001", FirstName = "Test", LastName = "Patient",
            DateOfBirth = new DateOnly(1970, 1, 1), Gender = "F", CreatedAt = DateTimeOffset.UtcNow
        };
        var encounter = new Encounter
        {
            Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient,
            Status = EncounterStatus.Active, Department = Department.Icu,
            AttendingPhysician = "Dr. Test", AdmittedAt = DateTimeOffset.UtcNow,
            CreatedAt = DateTimeOffset.UtcNow
        };
        db.Patients.Add(patient);
        db.Encounters.Add(encounter);
        db.AlertThresholds.AddRange(
            new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE",
                DisplayName = "Heart Rate", Unit = "bpm",
                CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150,
                CreatedAt = DateTimeOffset.UtcNow },
            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 }
        );
        await db.SaveChangesAsync();

        // Pre-load thresholds into Redis (replicates what ThresholdCacheLoader does at startup)
        var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
        var cache = redis.GetDatabase(1);
        await cache.StringSetAsync("threshold:HEART_RATE",
            """{"ObservationCode":"HEART_RATE","CriticalLow":30,"WarningLow":50,"WarningHigh":100,"CriticalHigh":150}""");
        await cache.StringSetAsync("threshold:POTASSIUM_MEQ_L",
            """{"ObservationCode":"POTASSIUM_MEQ_L","CriticalLow":2.5,"WarningLow":3.5,"WarningHigh":5.0,"CriticalHigh":6.5}""");

        _patientId = patient.Id;
        _encounterId = encounter.Id;
    }

    public Task DisposeAsync() => Task.CompletedTask;

    // Test 1: normal observation — no alert created
    [Fact]
    public async Task NormalObservation_NoAlert_Created()
    {
        var resp = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 78, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow, null)
            }));

        resp.StatusCode.Should().Be(HttpStatusCode.Created);
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        body!.RootElement.GetProperty("data").GetProperty("alertGenerated").GetBoolean()
            .Should().BeFalse();
        body.RootElement.GetProperty("data").GetProperty("duplicate").GetBoolean()
            .Should().BeFalse();

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var alertCount = await db.ClinicalAlerts.CountAsync();
        alertCount.Should().Be(0, "a normal value must not create an alert");

        // Outbox event should still be written for the Kafka consumer (warning detection)
        var outboxCount = await db.OutboxEvents.CountAsync();
        outboxCount.Should().Be(1, "observation.recorded outbox event must be written for every observation");
        var outboxTopic = await db.OutboxEvents.Select(e => e.Topic).FirstAsync();
        outboxTopic.Should().Be("observation.recorded");
    }

    // Test 2: critical threshold breach — alert and outbox event in same transaction
    [Fact]
    public async Task CriticalBreach_AlertCreated_InSameTransaction()
    {
        // Potassium 2.1 mEq/L is below critical_low of 2.5 — immediately life-threatening
        var resp = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("POTASSIUM_MEQ_L", 2.1m, "mEq/L", ObservationSource.Lab, DateTimeOffset.UtcNow, "key-critical-001")
            }));

        resp.StatusCode.Should().Be(HttpStatusCode.Created);
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        body!.RootElement.GetProperty("data").GetProperty("alertGenerated").GetBoolean()
            .Should().BeTrue();

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var alert = await db.ClinicalAlerts.SingleAsync();
        alert.Severity.Should().Be(AlertSeverity.Critical);
        alert.Status.Should().Be(AlertStatus.Open);
        alert.EncounterId.Should().Be(_encounterId);
        alert.PatientId.Should().Be(_patientId);

        // Both outbox events must exist: alert.generated AND observation.recorded
        var topics = await db.OutboxEvents.Select(e => e.Topic).OrderBy(t => t).ToListAsync();
        topics.Should().BeEquivalentTo(new[] { "alert.generated", "observation.recorded" });
    }

    // Test 3: warning threshold breach — no alert created; only observation.recorded outbox event
    [Fact]
    public async Task WarningBreach_NoAlertCreated_OnlyObservationOutboxEvent()
    {
        // Heart rate 104 bpm is above warning_high (100) but below critical_high (150)
        var resp = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 104, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow, null)
            }));

        resp.StatusCode.Should().Be(HttpStatusCode.Created);

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var alertCount = await db.ClinicalAlerts.CountAsync();
        alertCount.Should().Be(0, "warning detection is deferred to the Kafka consumer in Phase 3");

        var topics = await db.OutboxEvents.Select(e => e.Topic).ToListAsync();
        topics.Should().ContainSingle().Which.Should().Be("observation.recorded");
    }

    // Test 4: duplicate idempotency key — returns 201 with original observation, no duplicate
    [Fact]
    public async Task DuplicateIdempotencyKey_Returns201_NoDuplicateRow()
    {
        var payload = new BatchIngestRequest(new List<IngestObservationRequest>
        {
            new("HEART_RATE", 78, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow, "device-key-abc123")
        });

        var resp1 = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations", payload);
        var resp2 = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations", payload);

        resp1.StatusCode.Should().Be(HttpStatusCode.Created);
        resp2.StatusCode.Should().Be(HttpStatusCode.Created);

        var body2 = await resp2.Content.ReadFromJsonAsync<JsonDocument>();
        body2!.RootElement.GetProperty("data").GetProperty("duplicate").GetBoolean()
            .Should().BeTrue("the second call must be identified as a duplicate");

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var count = await db.Observations.CountAsync();
        count.Should().Be(1, "only one observation row must exist despite two identical requests");
    }

    // Test 5: discharged encounter — 409 returned, no observation written
    [Fact]
    public async Task DischargedEncounter_Returns409_NoObservationWritten()
    {
        // Discharge the encounter
        var patchResp = await _client.PatchAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/status",
            new { status = "Discharged" });
        patchResp.StatusCode.Should().Be(HttpStatusCode.OK);

        // Attempt to ingest against a discharged encounter
        var resp = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 78, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow, null)
            }));

        resp.StatusCode.Should().Be(HttpStatusCode.Conflict);
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        body!.RootElement.GetProperty("error").GetProperty("code").GetString()
            .Should().Be("ENCOUNTER_NOT_ACTIVE");

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var count = await db.Observations.CountAsync();
        count.Should().Be(0, "no observation must be written when encounter is not active");
    }

    // Test 6: plausibility violation — 422 returned, no rows written
    [Fact]
    public async Task ImplausibleValue_Returns422_NoRowsWritten()
    {
        // Heart rate of 350 bpm: above plausibility ceiling of 300
        var resp = await _client.PostAsJsonAsync(
            $"/api/v1/encounters/{_encounterId}/observations",
            new BatchIngestRequest(new List<IngestObservationRequest>
            {
                new("HEART_RATE", 350, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow, null)
            }));

        resp.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
        var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
        body!.RootElement.GetProperty("error").GetProperty("code").GetString()
            .Should().Be("OBSERVATION_OUT_OF_PLAUSIBLE_RANGE");

        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        (await db.Observations.CountAsync()).Should().Be(0);
        (await db.OutboxEvents.CountAsync()).Should().Be(0);
    }
}