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


/// <summary>
/// Integration tests for Phase 3: verification, rejection, separation of duties,
/// and work queues. Uses ApiFixture with real PostgreSQL and Redis from Phase 2.
/// </summary>
[Collection("Database")]
public class VerificationTests : IAsyncLifetime
{
    private readonly ApiFixture _fixture;
    private readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    public VerificationTests(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);
    }

    public Task DisposeAsync() => Task.CompletedTask;

    // ---------------------------------------------------------------
    // Separation of duties
    // ---------------------------------------------------------------

    /// <summary>
    /// Test: entry1 entered the batch. verifier1 attempts to verify a batch they
    /// also entered (EnteredByUserId = verifier1). Returns 409 SEPARATION_OF_DUTIES_VIOLATION.
    /// </summary>
    [Fact]
    public async Task Verify_SameUserAsEntryClerk_Returns409SeparationOfDuties()
    {
        // Arrange: batch entered by verifier1 — same user will attempt verification
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var verifierId = await BatchSeedHelper.UserIdAsync(db, "verifier1");
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, verifierId);

        // Act: verifier1 tries to verify their own batch
        var client = await AuthHelper.LoginAsync(_fixture, "verifier1");

        var request = new VerifyBatchRequest(
            new List<FieldCheck>
            {
                new("patient.fullName", "ok", null),
                new("observation.heartRate", "ok", null)
            },
            Passed: true
        );

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", request);

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

        var body = await response.Content.ReadAsStringAsync();
        body.Should().Contain("SEPARATION_OF_DUTIES_VIOLATION");
    }

    /// <summary>
    /// Test: Entry clerk A submits a batch. Verifier B (a different person) verifies it.
    /// The system accepts the verification because the verifier is not the same user
    /// who entered the data.
    ///
    /// Uses a VitalsSheet batch type which requires clinical approval per site config,
    /// so the expected target status is AwaitingClinicalApproval.
    /// </summary>
    [Fact]
    public async Task Verify_DifferentUserFromEntryClerk_Succeeds()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")), BatchType.VitalsSheet);

        // Act: VerifierB verifies the batch (different user from EntryClerkA)
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new VerifyBatchRequest(
            new List<FieldCheck>
            {
                new("patient.fullName", "ok", null),
                new("observation.heartRate", "ok", "Within normal range"),
                new("observation.bloodPressureSystolic", "warning", "Slightly elevated but plausible")
            },
            Passed: true
        );

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", request);

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

        // Reload batch from database to verify status transition
        var updatedBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);

        // VitalsSheet requires clinical approval per site config
        updatedBatch.Status.Should().Be(BatchStatus.AwaitingClinicalApproval);
        updatedBatch.VerifiedByUserId.Should().Be((await BatchSeedHelper.UserIdAsync(db, "verifier2")));

        // Verify event was written
        var events = await db.DigitizationEvents
            .Where(e => e.BatchId == batch.Id && e.EventType == DigitizationEventType.VerifiedPendingClinical)
            .ToListAsync();
        events.Should().HaveCount(1);
        events[0].ActorUserId.Should().Be((await BatchSeedHelper.UserIdAsync(db, "verifier2")));
        events[0].MetadataJson.Should().Contain("fieldChecks");
    }

    /// <summary>
    /// Test: Verify a PatientRegistration batch (clinical approval NOT required).
    /// The batch should transition directly to Verified, not AwaitingClinicalApproval.
    /// </summary>
    [Fact]
    public async Task Verify_NoClinicalApprovalRequired_TransitionsToVerified()
    {
        // Arrange: PatientRegistration does NOT require clinical approval
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")), BatchType.PatientRegistration);

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new VerifyBatchRequest(
            new List<FieldCheck>
            {
                new("patient.fullName", "ok", null),
                new("patient.dateOfBirth", "ok", null)
            },
            Passed: true
        );

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", request);

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

        var updatedBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);

        updatedBatch.Status.Should().Be(BatchStatus.Verified);

        var events = await db.DigitizationEvents
            .Where(e => e.BatchId == batch.Id && e.EventType == DigitizationEventType.Verified)
            .ToListAsync();
        events.Should().HaveCount(1);
    }

    // ---------------------------------------------------------------
    // Rejection
    // ---------------------------------------------------------------

    /// <summary>
    /// Test: Verifier B rejects a batch with a reason. The batch transitions to
    /// Rejected, the rejection reason is stored, and the batch appears in the
    /// entry work queue for re-entry.
    /// </summary>
    [Fact]
    public async Task Reject_WithValidReason_TransitionsToRejectedAndAppearsInEntryQueue()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act: VerifierB rejects
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new RejectBatchRequest(
            "Patient name does not match the scanned registration form. " +
            "Please re-enter the patient demographics from the chart.");

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/reject", request);

        // Assert — rejection accepted
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var updatedBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);
        updatedBatch.Status.Should().Be(BatchStatus.Rejected);
        updatedBatch.RejectionReason.Should().Contain("Patient name does not match");

        // Assert — event written
        var events = await db.DigitizationEvents
            .Where(e => e.BatchId == batch.Id && e.EventType == DigitizationEventType.Rejected)
            .ToListAsync();
        events.Should().HaveCount(1);
        events[0].MetadataJson.Should().Contain("PENDING_VERIFICATION");

        // Assert — batch appears in entry work queue
        var entryClient = await AuthHelper.LoginAsync(_fixture, "entry1");

        var queueResponse = await entryClient.GetAsync("/api/v1/work-queue/entry");
        queueResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        var queueBody = await queueResponse.Content.ReadAsStringAsync();
        queueBody.Should().Contain(batch.Id.ToString());
    }

    /// <summary>
    /// Test: Rejection without a reason returns 422.
    /// </summary>
    [Fact]
    public async Task Reject_WithoutReason_Returns422()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new RejectBatchRequest("");

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/reject", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
        var body = await response.Content.ReadAsStringAsync();
        body.Should().Contain("REJECTION_REASON_REQUIRED");
    }

    /// <summary>
    /// Test: Rejection with a reason shorter than 10 characters returns 422.
    /// </summary>
    [Fact]
    public async Task Reject_WithShortReason_Returns422()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new RejectBatchRequest("Bad data");

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/reject", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
        var body = await response.Content.ReadAsStringAsync();
        body.Should().Contain("REJECTION_REASON_TOO_SHORT");
    }

    // ---------------------------------------------------------------
    // Rejection loop: reject → re-enter → re-submit → verify
    // ---------------------------------------------------------------

    /// <summary>
    /// Test: Full rejection loop.
    /// 1. Batch is in PendingVerification (entered by EntryClerkA).
    /// 2. VerifierB rejects it with a reason.
    /// 3. Batch status → Rejected, appears in entry queue.
    /// 4. EntryClerkA or EntryClerkB picks it up (status → InEntry, simulated).
    /// 5. Re-submits (status → PendingVerification, simulated).
    /// 6. VerifierB verifies it (should succeed this time).
    ///
    /// Steps 4-5 are simulated by directly updating the database since the
    /// re-entry and re-submission endpoints are Phase 2 code.
    /// </summary>
    [Fact]
    public async Task RejectionLoop_RejectThenReenterThenVerify_Succeeds()
    {
        // Arrange: batch in PendingVerification entered by EntryClerkA
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")), BatchType.PatientRegistration);

        var verifierClient = await AuthHelper.LoginAsync(_fixture, "verifier2");

        // Step 1: VerifierB rejects the batch
        var rejectRequest = new RejectBatchRequest(
            "Date of birth is clearly wrong — year 1899 is not plausible for a current patient.");

        var rejectResponse = await verifierClient.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/reject", rejectRequest);
        rejectResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        // Verify it's now Rejected
        var rejectedBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);
        rejectedBatch.Status.Should().Be(BatchStatus.Rejected);

        // Step 2: Verify it appears in the entry queue
        var entryClient = await AuthHelper.LoginAsync(_fixture, "entry2");

        var entryQueueResponse = await entryClient.GetAsync("/api/v1/work-queue/entry");
        var entryQueueBody = await entryQueueResponse.Content.ReadAsStringAsync();
        entryQueueBody.Should().Contain(batch.Id.ToString());

        // Step 3: Simulate re-entry by EntryClerkB (direct DB update)
        // In production, this would go through the Phase 2 draft entry endpoints
        var trackedBatch = await db.DigitizationBatches.FindAsync(batch.Id);
        trackedBatch!.Status = BatchStatus.InEntry;
        trackedBatch.EnteredByUserId = (await BatchSeedHelper.UserIdAsync(db, "entry2")); // Different clerk re-enters
        trackedBatch.RejectionReason = null;
        trackedBatch.UpdatedAt = DateTimeOffset.UtcNow;
        await db.SaveChangesAsync();

        // Step 4: Simulate re-submission (status → PendingVerification)
        trackedBatch.Status = BatchStatus.PendingVerification;
        trackedBatch.UpdatedAt = DateTimeOffset.UtcNow;
        await db.SaveChangesAsync();

        // Step 5: VerifierB verifies the corrected batch — should succeed
        // Note: EntryClerkB did the re-entry, so VerifierB (different user) can verify
        var verifyRequest = new VerifyBatchRequest(
            new List<FieldCheck>
            {
                new("patient.fullName", "ok", null),
                new("patient.dateOfBirth", "ok", "Corrected to 1989")
            },
            Passed: true
        );

        var verifyResponse = await verifierClient.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", verifyRequest);
        verifyResponse.StatusCode.Should().Be(HttpStatusCode.OK);

        // Final assertion: batch is now Verified (PatientRegistration doesn't need clinical)
        var finalBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);
        finalBatch.Status.Should().Be(BatchStatus.Verified);
        finalBatch.VerifiedByUserId.Should().Be((await BatchSeedHelper.UserIdAsync(db, "verifier2")));

        // Verify the full event trail
        var allEvents = await db.DigitizationEvents
            .Where(e => e.BatchId == batch.Id)
            .OrderBy(e => e.OccurredAt)
            .ToListAsync();

        // submitted_for_verification (seed), rejected, verified
        allEvents.Should().HaveCountGreaterThanOrEqualTo(3);
        allEvents.Last().EventType.Should().Be(DigitizationEventType.Verified);
    }

    // ---------------------------------------------------------------
    // Work queue tests
    // ---------------------------------------------------------------

    /// <summary>
    /// Test: Verification queue returns only PendingVerification batches,
    /// sorted by UpdatedAt ASC (oldest first).
    /// </summary>
    [Fact]
    public async Task VerificationQueue_ReturnsPendingVerificationBatches_SortedByAge()
    {
        // Arrange: seed two batches at different times
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var olderBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));
        var trackedOlder = await db.DigitizationBatches.FindAsync(olderBatch.Id);
        trackedOlder!.UpdatedAt = DateTimeOffset.UtcNow.AddHours(-2);
        await db.SaveChangesAsync();

        var newerBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry2")));
        var trackedNewer = await db.DigitizationBatches.FindAsync(newerBatch.Id);
        trackedNewer!.UpdatedAt = DateTimeOffset.UtcNow.AddHours(-1);
        await db.SaveChangesAsync();

        // Also seed a rejected batch — should NOT appear in verification queue
        var rejectedBatch = await BatchSeedHelper.SeedBatchInRejectedAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier1");

        var response = await client.GetAsync("/api/v1/work-queue/verification");

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

        var body = await response.Content.ReadAsStringAsync();
        var json = JsonDocument.Parse(body);
        var items = json.RootElement
            .GetProperty("data")
            .GetProperty("items");

        var batchIds = Enumerable.Range(0, items.GetArrayLength())
            .Select(i => items[i].GetProperty("batchId").GetString())
            .ToList();

        batchIds.Should().Contain(olderBatch.Id.ToString());
        batchIds.Should().Contain(newerBatch.Id.ToString());
        batchIds.Should().NotContain(rejectedBatch.Id.ToString());

        foreach (var i in Enumerable.Range(0, items.GetArrayLength()))
        {
            items[i].GetProperty("status").GetString()
                .Should().Be(BatchStatus.PendingVerification.ToDbString());
        }

        // Older batch should appear before newer batch (FIFO by UpdatedAt)
        batchIds.IndexOf(olderBatch.Id.ToString())
            .Should().BeLessThan(batchIds.IndexOf(newerBatch.Id.ToString()));
    }

    /// <summary>
    /// Test: Entry queue returns InEntry and Rejected batches, but NOT Uploaded
    /// (Uploaded batches must be assigned first, which transitions them to InEntry).
    /// </summary>
    [Fact]
    public async Task EntryQueue_ReturnsInEntryAndRejectedBatches_ExcludesUploaded()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        // Seed a rejected batch (should appear in entry queue)
        var rejectedBatch = await BatchSeedHelper.SeedBatchInRejectedAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Seed a PendingVerification batch (should NOT appear in entry queue)
        var pendingBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry2")));

        // Seed an Uploaded batch directly (should NOT appear — must be assigned first)
        var uploadedBatchId = Guid.NewGuid();
        db.DigitizationBatches.Add(new DigitizationBatch
        {
            Id = uploadedBatchId,
            Status = BatchStatus.Uploaded,
            BatchType = BatchType.EncounterSummary,
            Track = BatchTrack.Backfill,
            DocumentRef = $"scans/2026/01/{uploadedBatchId}/test.pdf",
            DocumentSha256 = Guid.NewGuid().ToString("N"),
            CreatedAt = DateTimeOffset.UtcNow,
            UpdatedAt = DateTimeOffset.UtcNow
        });
        db.ScannedDocuments.Add(new ScannedDocument
        {
            Id = Guid.NewGuid(),
            BatchId = uploadedBatchId,
            ObjectKey = $"scans/2026/01/{uploadedBatchId}/test.pdf",
            Sha256 = Guid.NewGuid().ToString("N"),
            ContentType = "application/pdf",
            FileSizeBytes = 512,
            UploadedAt = DateTimeOffset.UtcNow
        });
        await db.SaveChangesAsync();

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "entry1");

        var response = await client.GetAsync("/api/v1/work-queue/entry");

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

        var body = await response.Content.ReadAsStringAsync();
        var json = JsonDocument.Parse(body);
        var items = json.RootElement
            .GetProperty("data")
            .GetProperty("items");

        var batchIds = Enumerable.Range(0, items.GetArrayLength())
            .Select(i => items[i].GetProperty("batchId").GetString())
            .ToList();

        batchIds.Should().Contain(rejectedBatch.Id.ToString());
        batchIds.Should().NotContain(uploadedBatchId.ToString(),
            "Uploaded batches must be assigned before appearing in the entry queue");
        batchIds.Should().NotContain(pendingBatch.Id.ToString());

        var entryStatuses = new[]
        {
            BatchStatus.InEntry.ToDbString(),
            BatchStatus.Rejected.ToDbString()
        };

        foreach (var i in Enumerable.Range(0, items.GetArrayLength()))
        {
            entryStatuses.Should().Contain(items[i].GetProperty("status").GetString());
        }
    }

    // ---------------------------------------------------------------
    // Status guard tests
    // ---------------------------------------------------------------

    /// <summary>
    /// Test: Attempting to verify a batch that is not in PendingVerification
    /// status returns 409 ILLEGAL_STATUS_TRANSITION.
    /// </summary>
    [Fact]
    public async Task Verify_BatchNotInPendingVerification_Returns409()
    {
        // Arrange: seed a batch in Rejected status
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInRejectedAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new VerifyBatchRequest(
            new List<FieldCheck> { new("patient.fullName", "ok", null) },
            Passed: true
        );

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Conflict);
        var body = await response.Content.ReadAsStringAsync();
        body.Should().Contain("ILLEGAL_STATUS_TRANSITION");
    }

    /// <summary>
    /// Test: Verification with passed=false transitions to Rejected with
    /// field-level error details as the rejection reason.
    /// </summary>
    [Fact]
    public async Task Verify_PassedFalse_TransitionsToRejectedWithFieldErrors()
    {
        // Arrange
        using var scope = _fixture.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var batch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
            db, (await BatchSeedHelper.UserIdAsync(db, "entry1")));

        // Act
        var client = await AuthHelper.LoginAsync(_fixture, "verifier2");

        var request = new VerifyBatchRequest(
            new List<FieldCheck>
            {
                new("patient.fullName", "ok", null),
                new("observation.heartRate", "error", "Value 350 is not physiologically possible"),
                new("observation.temperature", "error", "Missing unit — cannot verify")
            },
            Passed: false
        );

        var response = await client.PostAsJsonAsync(
            $"/api/v1/digitization-batches/{batch.Id}/verify", request);

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

        var updatedBatch = await db.DigitizationBatches
            .AsNoTracking()
            .FirstAsync(b => b.Id == batch.Id);

        updatedBatch.Status.Should().Be(BatchStatus.Rejected);
        updatedBatch.RejectionReason.Should().Contain("350 is not physiologically possible");
        updatedBatch.RejectionReason.Should().Contain("Missing unit");

        // Verify event metadata contains field checks
        var evt = await db.DigitizationEvents
            .Where(e => e.BatchId == batch.Id && e.EventType == DigitizationEventType.VerificationFailed)
            .FirstAsync();
        evt.MetadataJson.Should().Contain("fieldChecks");
        evt.MetadataJson.Should().Contain("errorCount");
    }
}