using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Lottery;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace HoneyBox.Tests.Services;

/// <summary>
/// 指定中奖奖品池过滤方法属性测试
/// </summary>
public class DesignatedPrizeFilterPropertyTests
{
    private readonly Mock<IInventoryManager> _mockInventoryManager = new();
    private readonly Mock<IRewardService> _mockRewardService = new();
    private readonly Mock<ILogger<LotteryEngine>> _mockLogger = new();

    private LotteryEngine CreateLotteryEngine()
    {
        var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        var dbContext = new HoneyBoxDbContext(options);
        return new LotteryEngine(dbContext, _mockInventoryManager.Object, _mockRewardService.Object, _mockLogger.Object);
    }

    private static List<PrizeProbability> CreatePrizePool(int count)
    {
        return Enumerable.Range(1, count).Select(i => new PrizeProbability
        {
            GoodsItemId = i,
            Title = $"Prize {i}",
            ImgUrl = $"http://test.com/prize{i}.jpg",
            ShangId = 1,
            SurplusStock = 10,
            Probability = 100m / count,
            Weight = 10,
            Price = 100,
            ScMoney = 50,
            GoodsType = 1,
            Doubling = 1,
            IsLingzhu = 0,
            ParentGoodsListId = 0
        }).ToList();
    }

    #region Property 3: Designated User Prize Pool Inclusion

    /// <summary>
    /// **Feature: designated-prize-winner, Property 3: Designated User Prize Pool Inclusion**
    /// For any designated user drawing from finite lottery, their prize pool SHALL include 
    /// all their designated prizes (where they are the designated user and is_active is true).
    /// **Validates: Requirements 2.3**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool DesignatedUser_ShouldSeeTheirDesignatedPrizes(PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount);
        var actualUserId = userId.Get;

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for the current user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: All designated prizes for this user should be in the filtered pool
        foreach (var kvp in designatedPrizes)
        {
            if (kvp.Value == actualUserId)
            {
                if (!filteredPool.Any(p => p.GoodsItemId == kvp.Key))
                {
                    return false;
                }
            }
        }

        return true;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 3: Designated User Prize Pool Inclusion**
    /// Designated user should also see all normal (non-designated) prizes.
    /// **Validates: Requirements 2.3**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool DesignatedUser_ShouldAlsoSeeNormalPrizes(PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount - 1);
        var actualUserId = userId.Get;

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for the current user (only first few)
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: All normal prizes should be in the filtered pool
        var normalPrizeIds = prizePool
            .Where(p => !designatedPrizes.ContainsKey(p.GoodsItemId))
            .Select(p => p.GoodsItemId)
            .ToList();

        foreach (var prizeId in normalPrizeIds)
        {
            if (!filteredPool.Any(p => p.GoodsItemId == prizeId))
            {
                return false;
            }
        }

        return true;
    }

    #endregion

    #region Property 4: Non-Designated User Prize Pool Exclusion (Finite)

    /// <summary>
    /// **Feature: designated-prize-winner, Property 4: Non-Designated User Prize Pool Exclusion (Finite)**
    /// For any non-designated user drawing from a finite lottery with normal prizes available, 
    /// their prize pool SHALL NOT contain any designated prizes.
    /// **Validates: Requirements 2.4**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool NonDesignatedUser_ShouldNotSeeOthersDesignatedPrizes_WhenNormalPrizesExist(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount - 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        // Ensure different users
        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for OTHER user (not current user)
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Designated prizes for other users should NOT be in the filtered pool
        // (unless fallback is triggered, which we check separately)
        var normalPrizesExist = prizePool.Any(p => !designatedPrizes.ContainsKey(p.GoodsItemId));
        
        if (normalPrizesExist)
        {
            foreach (var kvp in designatedPrizes)
            {
                if (filteredPool.Any(p => p.GoodsItemId == kvp.Key))
                {
                    return false;
                }
            }
        }

        return true;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 4: Non-Designated User Prize Pool Exclusion (Finite)**
    /// Non-designated user should see all normal prizes.
    /// **Validates: Requirements 2.4**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool NonDesignatedUser_ShouldSeeAllNormalPrizes(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount - 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for OTHER user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: All normal prizes should be in the filtered pool
        var normalPrizeIds = prizePool
            .Where(p => !designatedPrizes.ContainsKey(p.GoodsItemId))
            .Select(p => p.GoodsItemId)
            .ToList();

        foreach (var prizeId in normalPrizeIds)
        {
            if (!filteredPool.Any(p => p.GoodsItemId == prizeId))
            {
                return false;
            }
        }

        return true;
    }

    #endregion

    #region Property 6: Finite Lottery Fallback Mechanism

    /// <summary>
    /// **Feature: designated-prize-winner, Property 6: Finite Lottery Fallback Mechanism**
    /// For any finite lottery where only designated prizes remain (normal prize pool is empty), 
    /// a non-designated user SHALL be able to draw from all remaining prizes (fallback mechanism).
    /// **Validates: Requirements 2.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool FallbackMechanism_ShouldReturnAllPrizes_WhenOnlyDesignatedPrizesRemain(
        PositiveInt prizeCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(2, prizeCount.Get % 10 + 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // ALL prizes are designated to OTHER user (no normal prizes)
        var designatedPrizes = new Dictionary<int, int>();
        foreach (var prize in prizePool)
        {
            designatedPrizes[prize.GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Fallback should return ALL prizes
        return filteredPool.Count == prizePool.Count;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 6: Finite Lottery Fallback Mechanism**
    /// Fallback should NOT be triggered when normal prizes exist.
    /// **Validates: Requirements 2.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool FallbackMechanism_ShouldNotTrigger_WhenNormalPrizesExist(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount - 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Only SOME prizes are designated (leaving normal prizes)
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Filtered pool should NOT contain designated prizes (no fallback)
        var expectedCount = actualPrizeCount - actualDesignatedCount;
        return filteredPool.Count == expectedCount;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 6: Finite Lottery Fallback Mechanism**
    /// Empty designated prizes dictionary should return original pool unchanged.
    /// **Validates: Requirements 2.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool NoDesignatedPrizes_ShouldReturnOriginalPool(PositiveInt prizeCount, PositiveInt userId)
    {
        var actualPrizeCount = Math.Max(1, prizeCount.Get % 20 + 1);
        var actualUserId = userId.Get;

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);
        var designatedPrizes = new Dictionary<int, int>();

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        return filteredPool.Count == prizePool.Count;
    }

    #endregion

    #region Property 5: Non-Designated User Prize Pool Exclusion (Infinite - Strict)

    /// <summary>
    /// **Feature: designated-prize-winner, Property 5: Non-Designated User Prize Pool Exclusion (Infinite - Strict)**
    /// For any non-designated user drawing from an infinite lottery, their prize pool SHALL NEVER 
    /// contain designated prizes, even when no normal prizes exist.
    /// **Validates: Requirements 3.3, 3.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool InfiniteStrict_NonDesignatedUser_ShouldNeverSeeDesignatedPrizes(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for OTHER user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolStrict(prizePool, actualUserId, designatedPrizes);

        // Verify: NO designated prizes should be in the filtered pool (strict mode)
        foreach (var kvp in designatedPrizes)
        {
            if (filteredPool.Any(p => p.GoodsItemId == kvp.Key))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 5: Non-Designated User Prize Pool Exclusion (Infinite - Strict)**
    /// Even when ALL prizes are designated to others, strict mode should return empty pool (no fallback).
    /// **Validates: Requirements 3.3, 3.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool InfiniteStrict_ShouldReturnEmptyPool_WhenAllPrizesDesignatedToOthers(
        PositiveInt prizeCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(2, prizeCount.Get % 10 + 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // ALL prizes are designated to OTHER user
        var designatedPrizes = new Dictionary<int, int>();
        foreach (var prize in prizePool)
        {
            designatedPrizes[prize.GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolStrict(prizePool, actualUserId, designatedPrizes);

        // Verify: Strict mode should return EMPTY pool (no fallback)
        return filteredPool.Count == 0;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 5: Non-Designated User Prize Pool Exclusion (Infinite - Strict)**
    /// Designated user should still see their designated prizes in strict mode.
    /// **Validates: Requirements 3.4**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool InfiniteStrict_DesignatedUser_ShouldSeeTheirPrizes(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount);
        var actualUserId = userId.Get;

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for CURRENT user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualUserId;
        }

        var filteredPool = engine.FilterPrizePoolStrict(prizePool, actualUserId, designatedPrizes);

        // Verify: All designated prizes for this user should be in the filtered pool
        foreach (var kvp in designatedPrizes)
        {
            if (kvp.Value == actualUserId)
            {
                if (!filteredPool.Any(p => p.GoodsItemId == kvp.Key))
                {
                    return false;
                }
            }
        }

        return true;
    }

    #endregion

    #region Property 7: Probability Calculation Invariance

    /// <summary>
    /// **Feature: designated-prize-winner, Property 7: Probability Calculation Invariance**
    /// For any prize pool (filtered or unfiltered), the probability calculation method SHALL remain unchanged.
    /// Probabilities are calculated based on stock (finite) or real_pro (infinite) of the filtered pool.
    /// **Validates: Requirements 5.1, 5.3**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool ProbabilityCalculation_ShouldRemainUnchanged_AfterFiltering(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount - 1);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Create designated prizes for OTHER user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Total weight of filtered pool should equal sum of individual weights
        var totalWeight = filteredPool.Sum(p => p.Weight);
        var expectedTotalWeight = filteredPool.Count * 10; // Each prize has weight 10

        return Math.Abs(totalWeight - expectedTotalWeight) < 0.001;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 7: Probability Calculation Invariance**
    /// Filtering should not modify the original prize pool's probability values.
    /// **Validates: Requirements 5.1, 5.3**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool Filtering_ShouldNotModifyOriginalPrizePool(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId, PositiveInt otherUserId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount);
        var actualUserId = userId.Get;
        var actualOtherUserId = otherUserId.Get;

        if (actualUserId == actualOtherUserId)
        {
            actualOtherUserId = actualUserId + 1;
        }

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Store original values
        var originalWeights = prizePool.ToDictionary(p => p.GoodsItemId, p => p.Weight);
        var originalProbabilities = prizePool.ToDictionary(p => p.GoodsItemId, p => p.Probability);

        // Create designated prizes
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualOtherUserId;
        }

        // Apply filter
        _ = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Original pool values should be unchanged
        foreach (var prize in prizePool)
        {
            if (Math.Abs(prize.Weight - originalWeights[prize.GoodsItemId]) > 0.001)
            {
                return false;
            }
            if (Math.Abs(prize.Probability - originalProbabilities[prize.GoodsItemId]) > 0.001m)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 7: Probability Calculation Invariance**
    /// Each prize in the filtered pool should maintain its original weight value.
    /// **Validates: Requirements 5.1, 5.3**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool FilteredPrizes_ShouldMaintainOriginalWeights(
        PositiveInt prizeCount, PositiveInt designatedCount, PositiveInt userId)
    {
        var actualPrizeCount = Math.Max(5, prizeCount.Get % 20 + 1);
        var actualDesignatedCount = Math.Min(designatedCount.Get % 3 + 1, actualPrizeCount);
        var actualUserId = userId.Get;

        var engine = CreateLotteryEngine();
        var prizePool = CreatePrizePool(actualPrizeCount);

        // Store original weights
        var originalWeights = prizePool.ToDictionary(p => p.GoodsItemId, p => p.Weight);

        // Create designated prizes for CURRENT user
        var designatedPrizes = new Dictionary<int, int>();
        for (int i = 0; i < actualDesignatedCount; i++)
        {
            designatedPrizes[prizePool[i].GoodsItemId] = actualUserId;
        }

        var filteredPool = engine.FilterPrizePoolWithFallback(prizePool, actualUserId, designatedPrizes);

        // Verify: Each prize in filtered pool should have its original weight
        foreach (var prize in filteredPool)
        {
            if (Math.Abs(prize.Weight - originalWeights[prize.GoodsItemId]) > 0.001)
            {
                return false;
            }
        }

        return true;
    }

    #endregion
}
