using System.Security.Claims;
using System.Text.Json;
using AutoFixture;
using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;

namespace Bit.Api.Test.Controllers;

[ControllerCustomize(typeof(SyncController))]
[SutProviderCustomize]
public class SyncControllerTests
{
    [Theory]
    [BitAutoData]
    public async Task Get_ThrowBadRequest_WhenUserNotFound(SutProvider<SyncController> sutProvider)
    {
        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();

        async Task<SyncResponseModel> GetAction()
        {
            return await sutProvider.Sut.Get();
        }

        await Assert.ThrowsAsync<BadRequestException>((Func<Task<SyncResponseModel>>)GetAction);
    }

    [Theory]
    [BitAutoData]
    public async Task Get_Success_AtLeastOneEnabledOrg(User user,
        List<List<string>> userEquivalentDomains,
        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
        ICollection<ProviderUserProviderDetails> providerUserDetails,
        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
        ICollection<Folder> folders,
        ICollection<CipherDetails> ciphers,
        ICollection<Send> sends,
        ICollection<Policy> policies,
        ICollection<CollectionDetails> collections,
        SutProvider<SyncController> sutProvider)
    {
        // Get dependencies
        var userService = sutProvider.GetDependency<IUserService>();
        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
        var folderRepository = sutProvider.GetDependency<IFolderRepository>();
        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
        var sendRepository = sutProvider.GetDependency<ISendRepository>();
        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();

        // Adjust random data to match required formats / test intentions
        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);

        // At least 1 org needs to be enabled to fully test
        if (!organizationUserDetails.Any(o => o.Enabled))
        {
            // We need at least 1 enabled org
            if (organizationUserDetails.Count > 0)
            {
                organizationUserDetails.First().Enabled = true;
            }
            else
            {
                // create an enabled org
                var enabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();
                enabledOrg.Enabled = true;
                organizationUserDetails.Add((enabledOrg));
            }
        }

        // Setup returns
        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

        organizationUserRepository
            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);

        providerUserRepository
            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);

        providerUserRepository
            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
            .Returns(providerUserOrganizationDetails);

        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);

        sendRepository
            .GetManyByUserIdAsync(user.Id).Returns(sends);

        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);

        // Returns for methods only called if we have enabled orgs
        collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);
        collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());

        // Back to standard test setup
        userService.TwoFactorIsEnabledAsync(user).Returns(false);
        userService.HasPremiumFromOrganization(user).Returns(false);

        // Execute GET
        var result = await sutProvider.Sut.Get();


        // Asserts
        // Assert that methods are called
        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
        this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);

        Assert.IsType<SyncResponseModel>(result);

        // Collections should not be empty when at least 1 org is enabled
        Assert.NotEmpty(result.Collections);
    }


    [Theory]
    [BitAutoData]
    public async Task Get_Success_AllDisabledOrgs(User user,
        List<List<string>> userEquivalentDomains,
        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
        ICollection<ProviderUserProviderDetails> providerUserDetails,
        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
        ICollection<Folder> folders,
        ICollection<CipherDetails> ciphers,
        ICollection<Send> sends,
        ICollection<Policy> policies,
        SutProvider<SyncController> sutProvider)
    {
        // Get dependencies
        var userService = sutProvider.GetDependency<IUserService>();
        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
        var folderRepository = sutProvider.GetDependency<IFolderRepository>();
        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
        var sendRepository = sutProvider.GetDependency<ISendRepository>();
        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();

        // Adjust random data to match required formats / test intentions
        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);

        // All orgs disabled
        if (organizationUserDetails.Count > 0)
        {
            foreach (var orgUserDetails in organizationUserDetails)
            {
                orgUserDetails.Enabled = false;
            }
        }
        else
        {
            var disabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();
            disabledOrg.Enabled = false;
            organizationUserDetails.Add((disabledOrg));
        }


        // Setup returns
        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

        organizationUserRepository
            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);

        providerUserRepository
            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);

        providerUserRepository
            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
            .Returns(providerUserOrganizationDetails);

        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);

        sendRepository
            .GetManyByUserIdAsync(user.Id).Returns(sends);

        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);

        userService.TwoFactorIsEnabledAsync(user).Returns(false);
        userService.HasPremiumFromOrganization(user).Returns(false);

        // Execute GET
        var result = await sutProvider.Sut.Get();


        // Asserts
        // Assert that methods are called

        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
        this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);

        Assert.IsType<SyncResponseModel>(result);

        // Collections should be empty when all standard orgs are disabled.
        Assert.Empty(result.Collections);
    }


    // Test where provider org has specific plan type and assert plan type comes out on SyncResponseModel class on ProfileResponseModel
    [Theory]
    [BitAutoData]
    public async Task Get_ProviderPlanTypeProperlyPopulated(User user,
        List<List<string>> userEquivalentDomains,
        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,
        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,
        ICollection<ProviderUserProviderDetails> providerUserDetails,
        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
        ICollection<Folder> folders,
        ICollection<CipherDetails> ciphers,
        ICollection<Send> sends,
        ICollection<Policy> policies,
        ICollection<CollectionDetails> collections,
        SutProvider<SyncController> sutProvider)
    {
        // Get dependencies
        var userService = sutProvider.GetDependency<IUserService>();
        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
        var folderRepository = sutProvider.GetDependency<IFolderRepository>();
        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();
        var sendRepository = sutProvider.GetDependency<ISendRepository>();
        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();

        // Adjust random data to match required formats / test intentions
        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);


        // Setup returns
        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

        organizationUserRepository
            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);

        providerUserRepository
            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);

        providerUserRepository
            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)
            .Returns(providerUserOrganizationDetails);

        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);
        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);

        sendRepository
            .GetManyByUserIdAsync(user.Id).Returns(sends);

        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);

        // Returns for methods only called if we have enabled orgs
        collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);
        collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());

        // Back to standard test setup
        userService.TwoFactorIsEnabledAsync(user).Returns(false);
        userService.HasPremiumFromOrganization(user).Returns(false);

        // Execute GET
        var result = await sutProvider.Sut.Get();

        // Asserts
        // Assert that methods are called

        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
        this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository,
            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);

        Assert.IsType<SyncResponseModel>(result);

        // Look up ProviderOrg output and compare to ProviderOrg method inputs to ensure
        // product type is set correctly.
        foreach (var profProviderOrg in result.Profile.ProviderOrganizations)
        {
            var matchedProviderUserOrgDetails =
                providerUserOrganizationDetails.FirstOrDefault(p => p.OrganizationId == profProviderOrg.Id);

            if (matchedProviderUserOrgDetails != null)
            {
                var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).Product;
                Assert.Equal(providerOrgProductType, profProviderOrg.PlanProductType);
            }
        }
    }


    private async void AssertMethodsCalledAsync(IUserService userService,
        IOrganizationUserRepository organizationUserRepository,
        IProviderUserRepository providerUserRepository, IFolderRepository folderRepository,
        ICipherRepository cipherRepository, ISendRepository sendRepository,
        ICollectionRepository collectionRepository,
        ICollectionCipherRepository collectionCipherRepository,
        bool hasEnabledOrgs)
    {
        await userService.ReceivedWithAnyArgs(1).GetUserByPrincipalAsync(default);
        await organizationUserRepository.ReceivedWithAnyArgs(1)
            .GetManyDetailsByUserAsync(default);
        await providerUserRepository.ReceivedWithAnyArgs(1)
            .GetManyDetailsByUserAsync(default);
        await providerUserRepository.ReceivedWithAnyArgs(1)
            .GetManyOrganizationDetailsByUserAsync(default);

        await folderRepository.ReceivedWithAnyArgs(1)
            .GetManyByUserIdAsync(default);

        await cipherRepository.ReceivedWithAnyArgs(1)
            .GetManyByUserIdAsync(default);

        await sendRepository.ReceivedWithAnyArgs(1)
            .GetManyByUserIdAsync(default);

        // These two are only called when at least 1 enabled org.
        if (hasEnabledOrgs)
        {
            await collectionRepository.ReceivedWithAnyArgs(1)
                .GetManyByUserIdAsync(default);
            await collectionCipherRepository.ReceivedWithAnyArgs(1)
                .GetManyByUserIdAsync(default);
        }
        else
        {
            // all disabled orgs
            await collectionRepository.ReceivedWithAnyArgs(0)
                .GetManyByUserIdAsync(default);
            await collectionCipherRepository.ReceivedWithAnyArgs(0)
                .GetManyByUserIdAsync(default);
        }

        await userService.ReceivedWithAnyArgs(1)
            .TwoFactorIsEnabledAsync(default);
        await userService.ReceivedWithAnyArgs(1)
            .HasPremiumFromOrganization(default);
    }
}