using Dapper;
using KArtSell.BuildingBlocks.Data;
using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.Features.ApproveModel;
using KArtSell.Modules.ModelOperations.Features.RejectModel;
using Npgsql;
using Xunit;
using ApproveHandler = KArtSell.Modules.ModelOperations.Features.ApproveModel.Handler;
using RejectHandler = KArtSell.Modules.ModelOperations.Features.RejectModel.Handler;
using ApproveRequest = KArtSell.Modules.ModelOperations.Features.ApproveModel.Request;
using RejectRequest = KArtSell.Modules.ModelOperations.Features.RejectModel.Request;

namespace KArtSell.Integration.Tests;

/// <summary>
/// Approval Workflow Tests
/// Covers: Approval queue, maker-checker pattern, status transitions, validation
/// Following AGENTS.md v16.0: Workflow correctness, constraint enforcement, audit trails
/// </summary>
public sealed class ApprovalWorkflowTests : IAsyncLifetime
{
    private readonly string _connectionString;
    private readonly NpgsqlDataSource _dataSource;
    private readonly IDbConnectionFactory _connectionFactory;
    private readonly IClock _clock = new SystemClock();

    public ApprovalWorkflowTests()
    {
        _connectionString = TestDatabaseConnection.GetConnectionString();
        _dataSource = new NpgsqlDataSourceBuilder(_connectionString).Build();
        _connectionFactory = new NpgsqlConnectionFactory(_dataSource);
    }

    public async Task InitializeAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT 1";
        await cmd.ExecuteScalarAsync();
    }

    public async Task DisposeAsync()
    {
        await _dataSource.DisposeAsync();
    }

    /// <summary>
    /// Gate 4.1: Approval Queue List - Retrieve pending and completed approvals
    /// </summary>
    [Fact]
    public async Task ApprovalQueue_RetrievePending_ByStatus()
    {
        // Arrange: Create shadow_run and approval_queue records
        var runId1 = Guid.NewGuid();
        var runId2 = Guid.NewGuid();
        var modelId = Guid.NewGuid();
        var approverId = Guid.NewGuid();
        var now = _clock.UtcNow.DateTime;

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        // Create shadow runs
        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new[]
            {
                new { RunId = runId1, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) },
                new { RunId = runId2, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) }
            });

        // Create approvals: one Pending, one Approved
        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status)
            VALUES (@RunId, @ModelId, 'Pending')
            """,
            new { RunId = runId1, ModelId = modelId });

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status, approved_by, approval_reason, requested_at)
            VALUES (@RunId, @ModelId, 'Approved', @ApproverId, 'Validation gates passed', @Now)
            """,
            new { RunId = runId2, ModelId = modelId, ApproverId = approverId, Now = now });

        // Act: Query approval queue
        var queue = await connection.QueryAsync<(Guid RunId, string Status)>("""
            SELECT run_id AS RunId, status AS Status
            FROM model_operations.approval_queue
            ORDER BY status, requested_at DESC
            """);

        // Assert: Both records present, ordered by status (Approved after Pending)
        Assert.NotEmpty(queue);
        Assert.Contains(queue, r => r.Status == "Pending");
        Assert.Contains(queue, r => r.Status == "Approved");
    }

    /// <summary>
    /// Gate 4.2: Approve Model - Maker-checker approval with validation
    /// </summary>
    [Fact]
    public async Task ApprovalWorkflow_Approve_UpdatesStatusAndApprover()
    {
        // Arrange: Create shadow_run and pending approval
        var runId = Guid.NewGuid();
        var modelId = Guid.NewGuid();
        var approverId = Guid.NewGuid();
        var approvalReason = "Model passes all validation gates";

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new { RunId = runId, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) });

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status)
            VALUES (@RunId, @ModelId, 'Pending')
            """,
            new { RunId = runId, ModelId = modelId });

        // Act: Approve model
        var handler = new ApproveHandler(_connectionFactory, _clock);
        var request = new ApproveRequest(runId, approvalReason);
        var response = await handler.HandleAsync(request, approverId, CancellationToken.None);

        // Assert: Approval status changed to Approved
        Assert.Equal("Approved", response.Status);
        Assert.Equal(runId, response.RunId);

        // Verify database state
        var approval = await connection.QuerySingleAsync<(string Status, Guid ApprovedBy, string ApprovalReason, DateTime ApprovedAt)>("""
            SELECT status, approved_by, approval_reason, approved_at
            FROM model_operations.approval_queue WHERE run_id = @RunId
            """,
            new { RunId = runId });

        Assert.Equal("Approved", approval.Status);
        Assert.Equal(approverId, approval.ApprovedBy);
        Assert.Equal(approvalReason, approval.ApprovalReason);
        Assert.True(approval.ApprovedAt > DateTime.MinValue);
    }

    /// <summary>
    /// Gate 4.3: Approval Constraint - Cannot approve non-Pending approvals
    /// </summary>
    [Fact]
    public async Task ApprovalWorkflow_Approve_RejectsNonPendingApprovals()
    {
        // Arrange: Create already-approved approval
        var runId = Guid.NewGuid();
        var modelId = Guid.NewGuid();
        var approverId = Guid.NewGuid();

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new { RunId = runId, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) });

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status, approved_by, approval_reason)
            VALUES (@RunId, @ModelId, 'Approved', @ApproverId, 'Already approved')
            """,
            new { RunId = runId, ModelId = modelId, ApproverId = approverId });

        // Act: Try to approve already-approved approval
        var handler = new ApproveHandler(_connectionFactory, _clock);
        var request = new ApproveRequest(runId, "Trying to re-approve");

        // Assert: Handler rejects the operation
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => handler.HandleAsync(request, Guid.NewGuid(), CancellationToken.None));

        Assert.Contains("not Pending", ex.Message);
    }

    /// <summary>
    /// Gate 4.4: Reject Model - Alternative workflow for rejected approvals
    /// </summary>
    [Fact]
    public async Task ApprovalWorkflow_Reject_UpdatesStatusAndReason()
    {
        // Arrange: Create pending approval
        var runId = Guid.NewGuid();
        var modelId = Guid.NewGuid();
        var rejectionReason = "Model drift detected in OOS period";

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new { RunId = runId, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) });

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status)
            VALUES (@RunId, @ModelId, 'Pending')
            """,
            new { RunId = runId, ModelId = modelId });

        // Act: Reject model
        var handler = new RejectHandler(_connectionFactory, _clock);
        var request = new RejectRequest(runId, rejectionReason);
        var response = await handler.HandleAsync(request, Guid.NewGuid(), CancellationToken.None);

        // Assert: Approval status changed to Rejected
        Assert.Equal("Rejected", response.Status);
        Assert.Equal(runId, response.RunId);

        // Verify database state
        var approval = await connection.QuerySingleAsync<(string Status, string RejectionReason, DateTime RejectedAt)>("""
            SELECT status, rejection_reason, rejected_at
            FROM model_operations.approval_queue WHERE run_id = @RunId
            """,
            new { RunId = runId });

        Assert.Equal("Rejected", approval.Status);
        Assert.Equal(rejectionReason, approval.RejectionReason);
        Assert.True(approval.RejectedAt > DateTime.MinValue);
    }

    /// <summary>
    /// Gate 4.5: Approval Audit Trail - All state transitions tracked
    /// </summary>
    [Fact]
    public async Task ApprovalWorkflow_AuditTrail_TimestampsRecorded()
    {
        // Arrange: Create approval and track timestamps
        var runId = Guid.NewGuid();
        var modelId = Guid.NewGuid();
        var approverId = Guid.NewGuid();
        var beforeApproval = MarketTime.SeoulDateTime(_clock.UtcNow);

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new { RunId = runId, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) });

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status)
            VALUES (@RunId, @ModelId, 'Pending')
            """,
            new { RunId = runId, ModelId = modelId });

        // Act: Approve model
        var handler = new ApproveHandler(_connectionFactory, _clock);
        var request = new ApproveRequest(runId, "Approved");
        var response = await handler.HandleAsync(request, approverId, CancellationToken.None);

        var afterApproval = MarketTime.SeoulDateTime(_clock.UtcNow);

        // Assert: Timestamps are within expected range
        var approval = await connection.QuerySingleAsync<(DateTime RequestedAt, DateTime ApprovedAt)>("""
            SELECT requested_at, approved_at
            FROM model_operations.approval_queue WHERE run_id = @RunId
            """,
            new { RunId = runId });

        Assert.True(approval.RequestedAt <= approval.ApprovedAt);
        Assert.True(approval.ApprovedAt >= beforeApproval && approval.ApprovedAt <= afterApproval.AddSeconds(1));
    }

    /// <summary>
    /// Gate 4.6: Approval Idempotency - Cannot create duplicate approval for same run
    /// </summary>
    [Fact]
    public async Task ApprovalWorkflow_UniqueConstraint_PreventsDuplicateApprovals()
    {
        // Arrange: Create shadow_run
        var runId = Guid.NewGuid();
        var modelId = Guid.NewGuid();

        await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None);

        await connection.ExecuteAsync("""
            INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status)
            VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete')
            """,
            new { RunId = runId, ModelId = modelId, Start = new DateTime(2024, 1, 2), End = new DateTime(2024, 8, 31) });

        // Insert first approval
        await connection.ExecuteAsync("""
            INSERT INTO model_operations.approval_queue (run_id, model_id, status)
            VALUES (@RunId, @ModelId, 'Pending')
            """,
            new { RunId = runId, ModelId = modelId });

        // Act: Try to insert duplicate approval for same run
        var exception = await Assert.ThrowsAsync<PostgresException>(async () =>
        {
            await connection.ExecuteAsync("""
                INSERT INTO model_operations.approval_queue (run_id, model_id, status)
                VALUES (@RunId, @ModelId, 'Pending')
                """,
                new { RunId = runId, ModelId = modelId });
        });

        // Assert: UNIQUE constraint violation (run_id is unique)
        Assert.Contains("23505", exception.SqlState); // unique_violation error code
    }

    // ========== Helper Class ==========

    private sealed class NpgsqlConnectionFactory : IDbConnectionFactory
    {
        private readonly NpgsqlDataSource _dataSource;

        public NpgsqlConnectionFactory(NpgsqlDataSource dataSource)
        {
            _dataSource = dataSource;
        }

        public async ValueTask<System.Data.Common.DbConnection> OpenAsync(CancellationToken cancellationToken)
            => await _dataSource.OpenConnectionAsync(cancellationToken);
    }
}
