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

[Collection("Database")]
public class PromotionTests : IAsyncLifetime
{
    private readonly ApiFixture _fixture;
    private HttpClient _client = null!;

    public PromotionTests(ApiFixture fixture) => _fixture = fixture;

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

    public Task DisposeAsync() => Task.CompletedTask;

    /// <summary>
    /// Full pipeline: upload → entry → verify → approve → live observations exist, zero alerts for backfill.
    /// </summary>
    [Fact]
    public async Task Approve_BackfillBatch_CreatesLiveRecords_ZeroOutboxEvents()
    {
        // Arrange: drive batch through full lifecycle to verified status
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
            _fixture, track: "BACKFILL");

        // Act: approve as clinical approver (approver1)
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");

        var idempotencyKey = Guid.NewGuid().ToString();
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest(EnableRetroactiveAlerts: false));

        // Assert: HTTP 200
        approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = await approveResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        result.Should().NotBeNull();
        result!.Success.Should().BeTrue();
        result.Data.Should().NotBeNull();

        var promotion = result.Data!;
        promotion.BatchId.Should().Be(batchId);
        promotion.Status.Should().Be("PROMOTED");
        promotion.PatientId.Should().NotBeEmpty();
        promotion.Mrn.Should().StartWith("VCR-");
        promotion.EncounterId.Should().NotBeEmpty();
        promotion.ObservationIds.Should().HaveCount(3);
        promotion.OutboxEventsWritten.Should().Be(0,
            "backfill with enableRetroactiveAlerts=false should write zero outbox events");

        // Verify live records in database
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        // Patient exists with correct MRN
        var patient = await db.Patients.FirstOrDefaultAsync(p => p.Id == promotion.PatientId);
        patient.Should().NotBeNull();
        patient!.Mrn.Should().Be(promotion.Mrn);
        patient.FullName.Should().Be("TEST PATIENT");
        patient.DateOfBirth.Should().Be(new DateOnly(1990, 5, 15));

        // Encounter exists
        var encounter = await db.Encounters.FirstOrDefaultAsync(e => e.Id == promotion.EncounterId);
        encounter.Should().NotBeNull();
        encounter!.PatientId.Should().Be(promotion.PatientId);
        encounter.Department.Should().Be(Department.GeneralMedicine);

        // All 3 observations exist with correct source
        var observations = await db.Observations
            .Where(o => o.SourceBatchId == batchId)
            .ToListAsync();
        observations.Should().HaveCount(3);
        observations.Should().AllSatisfy(o =>
        {
            o.Source.Should().Be("digitization_backfill");
            o.PatientId.Should().Be(promotion.PatientId);
            o.EncounterId.Should().Be(promotion.EncounterId);
        });

        // Zero outbox events for this batch
        var observationIds = observations.Select(o => o.Id).ToList();
        var outboxEvents = await db.OutboxEvents
            .Where(e => observationIds.Contains(e.AggregateId))
            .ToListAsync();
        outboxEvents.Should().BeEmpty(
            "backfill with enableRetroactiveAlerts=false should not create outbox events");

        // Batch status is Promoted
        var batch = await db.DigitizationBatches.FirstAsync(b => b.Id == batchId);
        batch.Status.Should().Be(BatchStatus.Promoted);
        batch.PromotedAt.Should().NotBeNull();
        batch.PromotionEncounterId.Should().Be(promotion.EncounterId);

        // DigitizationEvent with type "promoted" exists
        var promotedEvent = await db.DigitizationEvents
            .FirstOrDefaultAsync(e => e.BatchId == batchId && e.EventType == DigitizationEventType.Promoted);
        promotedEvent.Should().NotBeNull();

        // Clean up header for other tests
        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// Backfill batch with enableRetroactiveAlerts=true should write outbox events for all observations.
    /// </summary>
    [Fact]
    public async Task Approve_BackfillWithRetroactiveAlerts_CreatesOutboxEvents()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
            _fixture, track: "BACKFILL");

        // Act
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");
        var idempotencyKey = Guid.NewGuid().ToString();
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest(EnableRetroactiveAlerts: true));

        // Assert
        approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = await approveResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        result!.Data!.OutboxEventsWritten.Should().Be(3,
            "backfill with enableRetroactiveAlerts=true should write one outbox event per observation");

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

        var observationIds = await db.Observations
            .Where(o => o.SourceBatchId == batchId)
            .Select(o => o.Id)
            .ToListAsync();

        var outboxEvents = await db.OutboxEvents
            .Where(e => observationIds.Contains(e.AggregateId))
            .ToListAsync();
        outboxEvents.Should().HaveCount(3);
        outboxEvents.Should().AllSatisfy(e =>
        {
            e.EventType.Should().Be("observation.created");
            e.AggregateType.Should().Be("Observation");
            e.ProcessedAt.Should().BeNull();
        });

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// Live-capture track always writes outbox events regardless of enableRetroactiveAlerts flag.
    /// </summary>
    [Fact]
    public async Task Approve_LiveCaptureBatch_AlwaysCreatesOutboxEvents()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
            _fixture, track: "LIVE_CAPTURE");

        // Act
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");
        var idempotencyKey = Guid.NewGuid().ToString();
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest(EnableRetroactiveAlerts: false));

        // Assert
        approveResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = await approveResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        result!.Data!.OutboxEventsWritten.Should().Be(3,
            "live_capture track should always write outbox events");

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

        var observations = await db.Observations
            .Where(o => o.SourceBatchId == batchId)
            .ToListAsync();
        observations.Should().AllSatisfy(o =>
            o.Source.Should().Be("live_capture"));

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// Idempotent promotion: approve twice with same Idempotency-Key produces one set of live rows.
    /// </summary>
    [Fact]
    public async Task Approve_SameIdempotencyKey_ReturnsIdenticalResult_NoDuplicateRows()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);

        _client = await AuthHelper.LoginAsync(_fixture, "approver1");
        var idempotencyKey = Guid.NewGuid().ToString();

        // Act: first approve
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var firstResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest(EnableRetroactiveAlerts: false));

        firstResponse.StatusCode.Should().Be(HttpStatusCode.OK);
        var firstResult = await firstResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        // Act: second approve with same key
        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var secondResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest(EnableRetroactiveAlerts: false));

        secondResponse.StatusCode.Should().Be(HttpStatusCode.OK);
        var secondResult = await secondResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        // Assert: identical results
        secondResult!.Data!.PatientId.Should().Be(firstResult!.Data!.PatientId);
        secondResult.Data.EncounterId.Should().Be(firstResult.Data.EncounterId);
        secondResult.Data.ObservationIds.Should().BeEquivalentTo(firstResult.Data.ObservationIds);
        secondResult.Data.Mrn.Should().Be(firstResult.Data.Mrn);

        // Assert: only one set of rows in database
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var observations = await db.Observations
            .Where(o => o.SourceBatchId == batchId)
            .ToListAsync();
        observations.Should().HaveCount(3,
            "idempotent replay should not create duplicate observations");

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// Separation of duties: entry clerk cannot approve their own batch.
    /// </summary>
    [Fact]
    public async Task Approve_ByEntryClerk_Returns409()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);

        // Act: try to approve as entry1 (who entered the data)
        _client = await AuthHelper.LoginAsync(_fixture, "entry1");
        _client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest());

        // Assert: either 403 (wrong role) or 409 (separation of duties)
        // entry1 has DataEntryClerk role, so the [Authorize(Roles = ...)] will reject with 403
        approveResponse.StatusCode.Should().Be(HttpStatusCode.Forbidden);

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// Missing Idempotency-Key header returns 400.
    /// </summary>
    [Fact]
    public async Task Approve_MissingIdempotencyKey_Returns400()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");

        // Act: approve without Idempotency-Key header
        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest());

        // Assert
        approveResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest);

        var result = await approveResponse.Content.ReadFromJsonAsync<ApiResponse<object>>();
        result!.Error!.Code.Should().Be("MISSING_IDEMPOTENCY_KEY");
    }

    /// <summary>
    /// Batch in wrong status (e.g., uploaded) returns 409.
    /// </summary>
    [Fact]
    public async Task Approve_WrongStatus_Returns409()
    {
        // Arrange: create a batch but don't verify it
        _client = await AuthHelper.LoginAsync(_fixture, "intake1");

        var fileContent = new ByteArrayContent(
            System.Text.Encoding.ASCII.GetBytes(
                $"%PDF-1.4\n%%EOF\n%test-{Guid.NewGuid()}"));
        fileContent.Headers.ContentType =
            new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");

        var formData = new MultipartFormDataContent
        {
            { fileContent, "file", "test.pdf" },
            { new StringContent("VITALS_SHEET"), "batchType" }
        };

        var uploadResponse = await _client.PostAsync(
            "/api/v1/digitization-batches", formData);
        uploadResponse.EnsureSuccessStatusCode();

        var uploadResult = await uploadResponse.Content.ReadFromJsonAsync<JsonElement>();
        var batchId = uploadResult.GetProperty("data").GetProperty("id").GetGuid();

        // Act: try to approve an uploaded batch
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");
        _client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest());

        // Assert
        approveResponse.StatusCode.Should().Be(HttpStatusCode.Conflict);

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");
    }

    /// <summary>
    /// GET /promotion-result returns correct data after promotion.
    /// </summary>
    [Fact]
    public async Task GetPromotionResult_AfterPromotion_ReturnsLiveIds()
    {
        // Arrange: create and promote a batch
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);

        _client = await AuthHelper.LoginAsync(_fixture, "approver1");
        var idempotencyKey = Guid.NewGuid().ToString();
        _client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

        var approveResponse = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId}/approve",
            new ApproveRequest());
        approveResponse.EnsureSuccessStatusCode();

        var approveResult = await approveResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        _client.DefaultRequestHeaders.Remove("Idempotency-Key");

        // Act: get promotion result
        var resultResponse = await _client.GetAsync(
            $"/api/v1/digitization-batches/{batchId}/promotion-result");

        // Assert
        resultResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = await resultResponse.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>();

        result!.Data!.BatchId.Should().Be(batchId);
        result.Data.PatientId.Should().Be(approveResult!.Data!.PatientId);
        result.Data.EncounterId.Should().Be(approveResult.Data.EncounterId);
        result.Data.ObservationIds.Should().HaveCount(3);
    }

    /// <summary>
    /// GET /promotion-result on non-promoted batch returns 409.
    /// </summary>
    [Fact]
    public async Task GetPromotionResult_NotPromoted_Returns409()
    {
        // Arrange
        var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
        _client = await AuthHelper.LoginAsync(_fixture, "approver1");

        // Act: query promotion result before approving
        var resultResponse = await _client.GetAsync(
            $"/api/v1/digitization-batches/{batchId}/promotion-result");

        // Assert
        resultResponse.StatusCode.Should().Be(HttpStatusCode.Conflict);
    }

    /// <summary>
    /// MRN generation produces unique, sequential values.
    /// </summary>
    [Fact]
    public async Task Approve_MultipleBatches_ProducesSequentialMrns()
    {
        // Arrange & Act: promote two batches with distinct patients so each gets a new MRN
        var batchId1 = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
        var batchId2 = await BatchPipelineHelper.CreateAndVerifyBatchAsync(
            _fixture, patientFullName: "Test Patient Two", patientDateOfBirth: "1991-06-20");

        _client = await AuthHelper.LoginAsync(_fixture, "approver1");

        _client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
        var result1 = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId1}/approve",
            new ApproveRequest());
        _client.DefaultRequestHeaders.Remove("Idempotency-Key");

        _client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
        var result2 = await _client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batchId2}/approve",
            new ApproveRequest());
        _client.DefaultRequestHeaders.Remove("Idempotency-Key");

        var promotion1 = (await result1.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>())!.Data!;
        var promotion2 = (await result2.Content
            .ReadFromJsonAsync<ApiResponse<PromotionResultResponse>>())!.Data!;

        // Assert: MRNs are unique
        promotion1.Mrn.Should().NotBe(promotion2.Mrn);
        promotion1.Mrn.Should().StartWith("VCR-");
        promotion2.Mrn.Should().StartWith("VCR-");

        // Assert: MRNs follow sequential pattern
        var mrn1Num = int.Parse(promotion1.Mrn.Split('-')[1]);
        var mrn2Num = int.Parse(promotion2.Mrn.Split('-')[1]);
        (mrn2Num - mrn1Num).Should().BeGreaterThanOrEqualTo(1);
    }
}