using System.Linq.Expressions;
using System.Text.Json;
using FluentAssertions;
using Hangfire;
using Hangfire.Common;
using Hangfire.States;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using RealCV.Application.DTOs;
using RealCV.Application.Interfaces;
using RealCV.Application.Models;
using RealCV.Domain.Entities;
using RealCV.Domain.Enums;
using RealCV.Infrastructure.Data;
using RealCV.Infrastructure.Jobs;
using RealCV.Infrastructure.Services;

namespace RealCV.Tests.Services;

public sealed class CVCheckServiceTests : IDisposable
{
    private readonly ApplicationDbContext _dbContext;
    private readonly Mock<IFileStorageService> _fileStorageServiceMock;
    private readonly Mock<IBackgroundJobClient> _backgroundJobClientMock;
    private readonly Mock<IAuditService> _auditServiceMock;
    private readonly Mock<ISubscriptionService> _subscriptionServiceMock;
    private readonly Mock<ILogger<CVCheckService>> _loggerMock;
    private readonly CVCheckService _sut;

    public CVCheckServiceTests()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        _dbContext = new ApplicationDbContext(options);
        _fileStorageServiceMock = new Mock<IFileStorageService>();
        _backgroundJobClientMock = new Mock<IBackgroundJobClient>();
        _auditServiceMock = new Mock<IAuditService>();
        _subscriptionServiceMock = new Mock<ISubscriptionService>();
        _loggerMock = new Mock<ILogger<CVCheckService>>();

        // Setup subscription service to allow checks by default
        _subscriptionServiceMock.Setup(x => x.CanPerformCheckAsync(It.IsAny<Guid>()))
            .ReturnsAsync(true);

        _sut = new CVCheckService(
            _dbContext,
            _fileStorageServiceMock.Object,
            _backgroundJobClientMock.Object,
            _auditServiceMock.Object,
            _subscriptionServiceMock.Object,
            _loggerMock.Object);
    }

    public void Dispose()
    {
        _dbContext.Dispose();
    }

    #region CreateCheckAsync Tests

    [Fact]
    public async Task CreateCheckAsync_CreatesRecordWithCorrectData()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var fileName = "test-cv.pdf";
        var expectedBlobUrl = "https://storage.blob.core.windows.net/cvs/test-cv.pdf";

        using var stream = new MemoryStream();
        _fileStorageServiceMock
            .Setup(x => x.UploadAsync(It.IsAny<Stream>(), fileName))
            .ReturnsAsync(expectedBlobUrl);

        // Act
        var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);

        // Assert
        var savedCheck = await _dbContext.CVChecks.FirstOrDefaultAsync(c => c.Id == checkId);

        savedCheck.Should().NotBeNull();
        savedCheck!.UserId.Should().Be(userId);
        savedCheck.OriginalFileName.Should().Be(fileName);
        savedCheck.BlobUrl.Should().Be(expectedBlobUrl);
        savedCheck.Status.Should().Be(CheckStatus.Pending);
        savedCheck.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task CreateCheckAsync_UploadsFileToBlobStorage()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var fileName = "resume.docx";
        var expectedBlobUrl = "https://storage.blob.core.windows.net/cvs/resume.docx";

        using var stream = new MemoryStream(new byte[] { 0x01, 0x02, 0x03 });
        _fileStorageServiceMock
            .Setup(x => x.UploadAsync(stream, fileName))
            .ReturnsAsync(expectedBlobUrl);

        // Act
        await _sut.CreateCheckAsync(userId, stream, fileName);

        // Assert
        _fileStorageServiceMock.Verify(
            x => x.UploadAsync(stream, fileName),
            Times.Once);
    }

    [Fact]
    public async Task CreateCheckAsync_QueuesHangfireBackgroundJob()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var fileName = "cv.pdf";

        using var stream = new MemoryStream();
        _fileStorageServiceMock
            .Setup(x => x.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>()))
            .ReturnsAsync("https://blob.url/cv.pdf");

        // Act
        var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);

        // Assert
        _backgroundJobClientMock.Verify(
            x => x.Create(
                It.Is<Job>(job =>
                    job.Type == typeof(ProcessCVCheckJob) &&
                    job.Method.Name == nameof(ProcessCVCheckJob.ExecuteAsync)),
                It.IsAny<EnqueuedState>()),
            Times.Once);
    }

    [Fact]
    public async Task CreateCheckAsync_ReturnsNewCheckId()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var fileName = "my-cv.pdf";

        using var stream = new MemoryStream();
        _fileStorageServiceMock
            .Setup(x => x.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>()))
            .ReturnsAsync("https://blob.url/my-cv.pdf");

        // Act
        var checkId = await _sut.CreateCheckAsync(userId, stream, fileName);

        // Assert
        checkId.Should().NotBe(Guid.Empty);

        var savedCheck = await _dbContext.CVChecks.FirstOrDefaultAsync(c => c.Id == checkId);
        savedCheck.Should().NotBeNull();
    }

    [Fact]
    public async Task CreateCheckAsync_ThrowsArgumentNullException_WhenFileIsNull()
    {
        // Arrange
        var userId = Guid.NewGuid();
        var fileName = "test.pdf";

        // Act
        var act = () => _sut.CreateCheckAsync(userId, null!, fileName);

        // Assert
        await act.Should().ThrowAsync<ArgumentNullException>()
            .WithParameterName("file");
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task CreateCheckAsync_ThrowsArgumentException_WhenFileNameIsNullOrWhitespace(string? fileName)
    {
        // Arrange
        var userId = Guid.NewGuid();
        using var stream = new MemoryStream();

        // Act
        var act = () => _sut.CreateCheckAsync(userId, stream, fileName!);

        // Assert
        await act.Should().ThrowAsync<ArgumentException>()
            .WithParameterName("fileName");
    }

    #endregion

    #region GetCheckAsync Tests

    [Fact]
    public async Task GetCheckAsync_ReturnsNull_WhenCheckDoesNotExist()
    {
        // Arrange
        var nonExistentId = Guid.NewGuid();

        // Act
        var result = await _sut.GetCheckAsync(nonExistentId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetCheckAsync_ReturnsCorrectDto_WhenCheckExists()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var createdAt = DateTime.UtcNow.AddHours(-1);
        var completedAt = DateTime.UtcNow.AddMinutes(-30);

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "professional-cv.pdf",
            BlobUrl = "https://blob.url/professional-cv.pdf",
            Status = CheckStatus.Completed,
            VeracityScore = 85,
            CreatedAt = createdAt,
            CompletedAt = completedAt
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetCheckAsync(checkId);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(checkId);
        result.OriginalFileName.Should().Be("professional-cv.pdf");
        result.Status.Should().Be("Completed");
        result.VeracityScore.Should().Be(85);
        result.CompletedAt.Should().Be(completedAt);
    }

    [Fact]
    public async Task GetCheckAsync_ReturnsPendingStatus_ForNewCheck()
    {
        // Arrange
        var checkId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = Guid.NewGuid(),
            OriginalFileName = "new-cv.pdf",
            BlobUrl = "https://blob.url/new-cv.pdf",
            Status = CheckStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetCheckAsync(checkId);

        // Assert
        result.Should().NotBeNull();
        result!.Status.Should().Be("Pending");
        result.VeracityScore.Should().BeNull();
        result.CompletedAt.Should().BeNull();
    }

    #endregion

    #region GetUserChecksAsync Tests

    [Fact]
    public async Task GetUserChecksAsync_ReturnsEmptyList_WhenUserHasNoChecks()
    {
        // Arrange
        var userId = Guid.NewGuid();

        // Act
        var result = await _sut.GetUserChecksAsync(userId);

        // Assert
        result.Should().NotBeNull();
        result.Should().BeEmpty();
    }

    [Fact]
    public async Task GetUserChecksAsync_ReturnsAllChecksForUser()
    {
        // Arrange
        var userId = Guid.NewGuid();

        var check1 = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            OriginalFileName = "cv1.pdf",
            BlobUrl = "https://blob.url/cv1.pdf",
            Status = CheckStatus.Completed,
            VeracityScore = 90,
            CreatedAt = DateTime.UtcNow.AddDays(-2)
        };

        var check2 = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            OriginalFileName = "cv2.pdf",
            BlobUrl = "https://blob.url/cv2.pdf",
            Status = CheckStatus.Pending,
            CreatedAt = DateTime.UtcNow.AddDays(-1)
        };

        var check3 = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            OriginalFileName = "cv3.pdf",
            BlobUrl = "https://blob.url/cv3.pdf",
            Status = CheckStatus.Processing,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.AddRange(check1, check2, check3);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetUserChecksAsync(userId);

        // Assert
        result.Should().HaveCount(3);
        result.Select(c => c.OriginalFileName)
            .Should().Contain(new[] { "cv1.pdf", "cv2.pdf", "cv3.pdf" });
    }

    [Fact]
    public async Task GetUserChecksAsync_DoesNotReturnOtherUsersChecks()
    {
        // Arrange
        var userId1 = Guid.NewGuid();
        var userId2 = Guid.NewGuid();

        var user1Check = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId1,
            OriginalFileName = "user1-cv.pdf",
            BlobUrl = "https://blob.url/user1-cv.pdf",
            Status = CheckStatus.Completed,
            CreatedAt = DateTime.UtcNow.AddHours(-1)
        };

        var user2Check1 = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId2,
            OriginalFileName = "user2-cv-a.pdf",
            BlobUrl = "https://blob.url/user2-cv-a.pdf",
            Status = CheckStatus.Pending,
            CreatedAt = DateTime.UtcNow.AddHours(-2)
        };

        var user2Check2 = new CVCheck
        {
            Id = Guid.NewGuid(),
            UserId = userId2,
            OriginalFileName = "user2-cv-b.pdf",
            BlobUrl = "https://blob.url/user2-cv-b.pdf",
            Status = CheckStatus.Completed,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.AddRange(user1Check, user2Check1, user2Check2);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetUserChecksAsync(userId1);

        // Assert
        result.Should().HaveCount(1);
        result.Single().OriginalFileName.Should().Be("user1-cv.pdf");
        result.Should().NotContain(c => c.OriginalFileName.StartsWith("user2"));
    }

    [Fact]
    public async Task GetUserChecksAsync_ReturnsChecksOrderedByCreatedAtDescending()
    {
        // Arrange
        var userId = Guid.NewGuid();

        // Create checks with specific IDs so we can update them after save
        var oldestId = Guid.NewGuid();
        var middleId = Guid.NewGuid();
        var newestId = Guid.NewGuid();

        var oldestCheck = new CVCheck
        {
            Id = oldestId,
            UserId = userId,
            OriginalFileName = "oldest.pdf",
            BlobUrl = "https://blob.url/oldest.pdf",
            Status = CheckStatus.Completed
        };

        var middleCheck = new CVCheck
        {
            Id = middleId,
            UserId = userId,
            OriginalFileName = "middle.pdf",
            BlobUrl = "https://blob.url/middle.pdf",
            Status = CheckStatus.Completed
        };

        var newestCheck = new CVCheck
        {
            Id = newestId,
            UserId = userId,
            OriginalFileName = "newest.pdf",
            BlobUrl = "https://blob.url/newest.pdf",
            Status = CheckStatus.Pending
        };

        // Add in random order - SaveChangesAsync will set CreatedAt to DateTime.UtcNow
        _dbContext.CVChecks.AddRange(middleCheck, newestCheck, oldestCheck);
        await _dbContext.SaveChangesAsync();

        // Detach all entities to allow direct update
        _dbContext.ChangeTracker.Clear();

        // Now update CreatedAt timestamps using raw SQL to bypass SaveChangesAsync override
        // Since InMemory database doesn't support raw SQL, we modify directly
        var oldest = await _dbContext.CVChecks.FindAsync(oldestId);
        var middle = await _dbContext.CVChecks.FindAsync(middleId);
        var newest = await _dbContext.CVChecks.FindAsync(newestId);

        oldest!.CreatedAt = DateTime.UtcNow.AddDays(-7);
        middle!.CreatedAt = DateTime.UtcNow.AddDays(-3);
        newest!.CreatedAt = DateTime.UtcNow;

        // Mark as modified (not added) to bypass the CreatedAt override
        _dbContext.Entry(oldest).State = EntityState.Modified;
        _dbContext.Entry(middle).State = EntityState.Modified;
        _dbContext.Entry(newest).State = EntityState.Modified;

        await _dbContext.SaveChangesAsync();
        _dbContext.ChangeTracker.Clear();

        // Act
        var result = await _sut.GetUserChecksAsync(userId);

        // Assert
        result.Should().HaveCount(3);
        result[0].OriginalFileName.Should().Be("newest.pdf");
        result[1].OriginalFileName.Should().Be("middle.pdf");
        result[2].OriginalFileName.Should().Be("oldest.pdf");
    }

    #endregion

    #region GetCheckForUserAsync Tests

    [Fact]
    public async Task GetCheckForUserAsync_ReturnsNull_WhenCheckDoesNotExist()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        // Act
        var result = await _sut.GetCheckForUserAsync(checkId, userId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetCheckForUserAsync_ReturnsNull_WhenCheckBelongsToDifferentUser()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var ownerId = Guid.NewGuid();
        var requestingUserId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = ownerId,
            OriginalFileName = "owner-cv.pdf",
            BlobUrl = "https://blob.url/owner-cv.pdf",
            Status = CheckStatus.Completed,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetCheckForUserAsync(checkId, requestingUserId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetCheckForUserAsync_ReturnsCheck_WhenCheckBelongsToUser()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "my-cv.pdf",
            BlobUrl = "https://blob.url/my-cv.pdf",
            Status = CheckStatus.Completed,
            VeracityScore = 88,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetCheckForUserAsync(checkId, userId);

        // Assert
        result.Should().NotBeNull();
        result!.Id.Should().Be(checkId);
        result.OriginalFileName.Should().Be("my-cv.pdf");
        result.VeracityScore.Should().Be(88);
    }

    #endregion

    #region GetReportAsync Tests

    [Fact]
    public async Task GetReportAsync_ReturnsNull_WhenCheckDoesNotExist()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        // Act
        var result = await _sut.GetReportAsync(checkId, userId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetReportAsync_ReturnsNull_WhenCheckBelongsToDifferentUser()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var ownerId = Guid.NewGuid();
        var requestingUserId = Guid.NewGuid();

        var report = CreateTestReport();
        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = ownerId,
            OriginalFileName = "cv.pdf",
            BlobUrl = "https://blob.url/cv.pdf",
            Status = CheckStatus.Completed,
            ReportJson = JsonSerializer.Serialize(report),
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetReportAsync(checkId, requestingUserId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetReportAsync_ReturnsNull_WhenCheckIsNotCompleted()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "cv.pdf",
            BlobUrl = "https://blob.url/cv.pdf",
            Status = CheckStatus.Processing,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetReportAsync(checkId, userId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetReportAsync_ReturnsNull_WhenReportJsonIsEmpty()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "cv.pdf",
            BlobUrl = "https://blob.url/cv.pdf",
            Status = CheckStatus.Completed,
            ReportJson = string.Empty,
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetReportAsync(checkId, userId);

        // Assert
        result.Should().BeNull();
    }

    [Fact]
    public async Task GetReportAsync_ReturnsReport_WhenCheckIsCompletedWithValidReport()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        var report = CreateTestReport();
        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "cv.pdf",
            BlobUrl = "https://blob.url/cv.pdf",
            Status = CheckStatus.Completed,
            ReportJson = JsonSerializer.Serialize(report),
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetReportAsync(checkId, userId);

        // Assert
        result.Should().NotBeNull();
        result!.OverallScore.Should().Be(85);
        result.ScoreLabel.Should().Be("Good");
    }

    [Fact]
    public async Task GetReportAsync_ReturnsNull_WhenReportJsonIsInvalid()
    {
        // Arrange
        var checkId = Guid.NewGuid();
        var userId = Guid.NewGuid();

        var cvCheck = new CVCheck
        {
            Id = checkId,
            UserId = userId,
            OriginalFileName = "cv.pdf",
            BlobUrl = "https://blob.url/cv.pdf",
            Status = CheckStatus.Completed,
            ReportJson = "{ invalid json }",
            CreatedAt = DateTime.UtcNow
        };

        _dbContext.CVChecks.Add(cvCheck);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.GetReportAsync(checkId, userId);

        // Assert
        result.Should().BeNull();
    }

    #endregion

    #region Helper Methods

    private static VeracityReport CreateTestReport()
    {
        return new VeracityReport
        {
            OverallScore = 85,
            ScoreLabel = "Good",
            EmploymentVerifications = [],
            TimelineAnalysis = new TimelineAnalysisResult
            {
                Gaps = [],
                Overlaps = [],
                TotalGapMonths = 0,
                TotalOverlapMonths = 0
            },
            Flags = [],
            GeneratedAt = DateTime.UtcNow
        };
    }

    #endregion
}
