using ApplicationLayer.InfrastructureServicesInterfaces;
using ApplicationLayer.Services.VisaApplications.NeededServices;
using Domains.VisaApplicationDomain;
using FluentAssertions;
using Infrastructure.Database;
using Infrastructure.Database.VisaApplications.Repositories;
using Infrastructure.Database.VisaApplications.Repositories.Exceptions;
using VisaApi.Fakers.Applicants;
using VisaApi.Fakers.Users;
using VisaApi.Fakers.VisaApplications;
using VisaApi.Services;
using Xunit;
namespace VisaApi.Tests.Infrastructure.Database.Repositories
{
    [Collection(Collections.ContextUsingTestCollection)]
    public class VisaApplicationsRepositoryTests
    {
        private readonly static UserFaker userFaker = new();
        private readonly static ApplicantFaker applicantFaker = new(GetDateTimeProvider());
        private readonly static VisaApplicationFaker applicationFaker = new(GetDateTimeProvider());
        ///  Returns  
        ///  Database context 
        /// Repository
        private static IVisaApplicationsRepository GetRepository(DbContext context)
            => new VisaApplicationsRepository(context, context);
        ///  Returns  
        private static IDateTimeProvider GetDateTimeProvider() => new TestDateTimeProvider();
        /// 
        /// Test for  method that should return empty if no applications added
        /// 
        [Fact]
        private async Task GetOfApplicantForEmptyShouldReturnEmpty()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            await context.SaveChangesAsync();
            var result = await repository.GetOfApplicantAsync(applicant.Id, CancellationToken.None);
            result.Should().BeEmpty();
        }
        /// 
        /// Test for  method that should return added entities
        /// 
        [Fact]
        private async Task GetOfApplicantForExistingShouldReturnEntities()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            var applications = new List();
            for (var i = 0; i < 5; i++)
            {
                var application = applicationFaker.GenerateValid(applicant);
                applications.Add(application);
                await context.AddAsync(application);
            }
            await context.SaveChangesAsync();
            var result = await repository.GetOfApplicantAsync(applicant.Id, CancellationToken.None);
            result.Should().Contain(applications).And.HaveSameCount(applications);
        }
        /// 
        /// Test for  method that should throw exception for not existing entities
        /// 
        [Fact]
        private async Task GetApplicantIdByUserIdForNotExistingShouldThrow()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            ApplicationNotFoundByApplicantAndApplicationIdException? result = null;
            await context.SaveChangesAsync();
            try
            {
                await repository.GetByApplicantAndApplicationIdAsync(Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None);
            }
            catch (Exception e)
            {
                result = e as ApplicationNotFoundByApplicantAndApplicationIdException;
            }
            result.Should().NotBeNull();
        }
        /// 
        /// Test for  method that should throw exception for not existing applicant
        /// 
        [Fact]
        private async Task GetApplicantIdByUserIdForNotExistingApplicantShouldThrow()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            var application = applicationFaker.GenerateValid(applicant);
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            await context.AddAsync(application);
            ApplicationNotFoundByApplicantAndApplicationIdException? result = null;
            await context.SaveChangesAsync();
            try
            {
                await repository.GetByApplicantAndApplicationIdAsync(Guid.NewGuid(), application.Id, CancellationToken.None);
            }
            catch (Exception e)
            {
                result = e as ApplicationNotFoundByApplicantAndApplicationIdException;
            }
            result.Should().NotBeNull();
        }
        /// 
        /// Test for  method that should throw exception for not existing application
        /// 
        [Fact]
        private async Task GetApplicantIdByUserIdForNotExistingApplicationShouldThrow()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            ApplicationNotFoundByApplicantAndApplicationIdException? result = null;
            await context.SaveChangesAsync();
            try
            {
                await repository.GetByApplicantAndApplicationIdAsync(applicant.Id, Guid.NewGuid(), CancellationToken.None);
            }
            catch (Exception e)
            {
                result = e as ApplicationNotFoundByApplicantAndApplicationIdException;
            }
            result.Should().NotBeNull();
        }
        /// 
        /// Test for  method
        /// that should throw exception for not accessible application
        /// 
        [Fact]
        private async Task GetApplicantIdByUserIdForNotAccessibleApplicationShouldThrow()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            var otherUser = userFaker.Generate();
            var otherApplicant = applicantFaker.Generate();
            otherApplicant.UserId = user.Id;
            var notAccessibleApplication = applicationFaker.GenerateValid(otherApplicant);
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            await context.AddAsync(otherUser);
            await context.AddAsync(otherApplicant);
            await context.AddAsync(notAccessibleApplication);
            ApplicationNotFoundByApplicantAndApplicationIdException? result = null;
            await context.SaveChangesAsync();
            try
            {
                await repository.GetByApplicantAndApplicationIdAsync(applicant.Id, notAccessibleApplication.Id, CancellationToken.None);
            }
            catch (Exception e)
            {
                result = e as ApplicationNotFoundByApplicantAndApplicationIdException;
            }
            result.Should().NotBeNull();
        }
        /// 
        /// Test for  method
        /// that should return application for valid identifiers
        /// 
        [Fact]
        private async Task GetApplicantIdByUserIdForValidIdsShouldReturnApplication()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            var application = applicationFaker.GenerateValid(applicant);
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            await context.AddAsync(application);
            await context.SaveChangesAsync();
            var result = await repository.GetByApplicantAndApplicationIdAsync(applicant.Id, application.Id, CancellationToken.None);
            result.Should().Be(application);
        }
        /// 
        /// Test for  method that should return empty from empty db
        /// 
        [Fact]
        private async Task GetPendingApplicationsForEmptyShouldReturnEmpty()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var result = await repository.GetPendingApplicationsAsync(CancellationToken.None);
            result.Should().BeEmpty();
        }
        /// 
        /// Test for  method that should return pending applications from not empty db
        /// 
        [Fact]
        private async Task GetPendingApplicationsForExistingShouldReturnExistingPending()
        {
            await using var context = InMemoryContextProvider.GetDbContext();
            var repository = GetRepository(context);
            var user = userFaker.Generate();
            var applicant = applicantFaker.Generate();
            applicant.UserId = user.Id;
            var applicationPending = applicationFaker.GenerateValid(applicant);
            applicationPending.Status = ApplicationStatus.Pending;
            var applicationNotPending = applicationFaker.GenerateValid(applicant);
            applicationNotPending.Status = ApplicationStatus.Approved;
            await context.AddAsync(user);
            await context.AddAsync(applicant);
            await context.AddAsync(applicationPending);
            await context.AddAsync(applicationNotPending);
            await context.SaveChangesAsync();
            var result = await repository.GetPendingApplicationsAsync(CancellationToken.None);
            result.Should().Contain(applicationPending).And.HaveCount(1);
        }
    }
}