using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Business.Models;
using HoneyBox.Admin.Business.Models.DesignatedPrize;
using HoneyBox.Admin.Business.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace HoneyBox.Tests.Services;

/// <summary>
/// DesignatedPrizeService 属性测试
/// </summary>
public class DesignatedPrizeServicePropertyTests
{
    private readonly Mock<ILogger<DesignatedPrizeService>> _mockLogger = new();

    private (HoneyBoxDbContext dbContext, DesignatedPrizeService service) CreateService()
    {
        var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        var dbContext = new HoneyBoxDbContext(options);
        var service = new DesignatedPrizeService(dbContext, _mockLogger.Object);
        return (dbContext, service);
    }

    private async Task<(int goodsId, List<int> prizeIds, int userId)> SeedTestDataAsync(HoneyBoxDbContext dbContext, int prizeCount)
    {
        // Create user
        var user = new User
        {
            OpenId = $"test_openid_{Guid.NewGuid()}",
            Uid = $"test_uid_{Guid.NewGuid()}",
            Nickname = "测试用户",
            HeadImg = "http://test.com/avatar.jpg",
            Mobile = "13800138000",
            CreatedAt = DateTime.Now,
            UpdatedAt = DateTime.Now
        };
        dbContext.Users.Add(user);
        await dbContext.SaveChangesAsync();

        // Create goods
        var goods = new Good
        {
            Title = "测试商品",
            ImgUrl = "http://test.com/goods.jpg",
            ImgUrlDetail = "http://test.com/goods_detail.jpg",
            Price = 100,
            Type = 1,
            Status = 1,
            Stock = 100,
            CreatedAt = DateTime.Now,
            UpdatedAt = DateTime.Now
        };
        dbContext.Goods.Add(goods);
        await dbContext.SaveChangesAsync();


        // Create prizes
        var prizeIds = new List<int>();
        for (int i = 0; i < prizeCount; i++)
        {
            var prize = new GoodsItem
            {
                GoodsId = goods.Id,
                Num = i + 1,
                Title = $"奖品{i + 1}",
                ImgUrl = $"http://test.com/prize{i + 1}.jpg",
                Stock = 1,
                SurplusStock = 1,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = 1,
                GoodsType = 1,
                PrizeCode = $"PC{i + 1:D3}",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.Add(prize);
            await dbContext.SaveChangesAsync();
            prizeIds.Add(prize.Id);
        }

        return (goods.Id, prizeIds, user.Id);
    }

    #region Property 1: Unique Constraint Enforcement

    /// <summary>
    /// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
    /// For any goods and goods_item combination, attempting to create a second designated prize 
    /// configuration SHALL result in an error, ensuring only one user can be designated per prize.
    /// **Validates: Requirements 1.2, 4.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool UniqueConstraint_ShouldPreventDuplicateConfiguration(PositiveInt prizeCount, PositiveInt userId1, PositiveInt userId2)
    {
        var actualPrizeCount = Math.Max(1, prizeCount.Get % 5 + 1);
        var actualUserId1 = userId1.Get;
        var actualUserId2 = userId2.Get;

        // Ensure different user IDs for the test
        if (actualUserId1 == actualUserId2)
        {
            actualUserId2 = actualUserId1 + 1;
        }

        var (dbContext, service) = CreateService();

        try
        {
            // Seed test data
            var (goodsId, prizeIds, _) = SeedTestDataAsync(dbContext, actualPrizeCount).GetAwaiter().GetResult();

            // Create additional users for the test
            var user1 = new User
            {
                OpenId = $"user1_{Guid.NewGuid()}",
                Uid = $"uid1_{Guid.NewGuid()}",
                Nickname = "用户1",
                HeadImg = "http://test.com/u1.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            var user2 = new User
            {
                OpenId = $"user2_{Guid.NewGuid()}",
                Uid = $"uid2_{Guid.NewGuid()}",
                Nickname = "用户2",
                HeadImg = "http://test.com/u2.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Users.AddRange(user1, user2);
            dbContext.SaveChanges();

            var targetPrizeId = prizeIds[0];

            // First creation should succeed
            var request1 = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = targetPrizeId,
                UserId = user1.Id,
                Remark = "First config"
            };
            var result1 = service.CreateAsync(goodsId, request1).GetAwaiter().GetResult();

            // Second creation with same goods_id and goods_item_id should fail
            var request2 = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = targetPrizeId,
                UserId = user2.Id,
                Remark = "Second config"
            };

            try
            {
                service.CreateAsync(goodsId, request2).GetAwaiter().GetResult();
                // If we reach here, the constraint was not enforced
                return false;
            }
            catch (BusinessException ex)
            {
                // Expected: Should throw BusinessException with Conflict code
                return ex.Code == BusinessErrorCodes.Conflict;
            }
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
    /// Different prizes in the same goods can each have their own designated user.
    /// **Validates: Requirements 1.2, 4.5**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool UniqueConstraint_ShouldAllowDifferentPrizesToHaveDifferentDesignatedUsers(PositiveInt prizeCount)
    {
        var actualPrizeCount = Math.Max(2, prizeCount.Get % 5 + 2);

        var (dbContext, service) = CreateService();

        try
        {
            // Seed test data
            var (goodsId, prizeIds, _) = SeedTestDataAsync(dbContext, actualPrizeCount).GetAwaiter().GetResult();

            // Create users for each prize
            var users = new List<User>();
            for (int i = 0; i < actualPrizeCount; i++)
            {
                var user = new User
                {
                    OpenId = $"user{i}_{Guid.NewGuid()}",
                    Uid = $"uid{i}_{Guid.NewGuid()}",
                    Nickname = $"用户{i}",
                    HeadImg = $"http://test.com/u{i}.jpg",
                    CreatedAt = DateTime.Now,
                    UpdatedAt = DateTime.Now
                };
                users.Add(user);
            }
            dbContext.Users.AddRange(users);
            dbContext.SaveChanges();

            // Each prize should be able to have its own designated user
            for (int i = 0; i < actualPrizeCount; i++)
            {
                var request = new CreateDesignatedPrizeRequest
                {
                    GoodsItemId = prizeIds[i],
                    UserId = users[i].Id,
                    Remark = $"Config for prize {i}"
                };

                try
                {
                    var result = service.CreateAsync(goodsId, request).GetAwaiter().GetResult();
                    if (result == null)
                    {
                        return false;
                    }
                }
                catch
                {
                    // Should not throw for different prizes
                    return false;
                }
            }

            // Verify all configurations were created
            var configs = service.GetByGoodsIdAsync(goodsId).GetAwaiter().GetResult();
            return configs.Count == actualPrizeCount;
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
    /// Same prize in different goods can have designated users (unique constraint is per goods+prize).
    /// **Validates: Requirements 1.2, 4.5**
    /// </summary>
    [Property(MaxTest = 50)]
    public bool UniqueConstraint_ShouldAllowSamePrizeIdInDifferentGoods(PositiveInt userId)
    {
        var (dbContext, service) = CreateService();

        try
        {
            // Create user
            var user = new User
            {
                OpenId = $"user_{Guid.NewGuid()}",
                Uid = $"uid_{Guid.NewGuid()}",
                Nickname = "测试用户",
                HeadImg = "http://test.com/u.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Users.Add(user);
            dbContext.SaveChanges();

            // Create two different goods
            var goods1 = new Good
            {
                Title = "商品1",
                ImgUrl = "http://test.com/g1.jpg",
                ImgUrlDetail = "http://test.com/g1_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            var goods2 = new Good
            {
                Title = "商品2",
                ImgUrl = "http://test.com/g2.jpg",
                ImgUrlDetail = "http://test.com/g2_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Goods.AddRange(goods1, goods2);
            dbContext.SaveChanges();

            // Create prizes for each goods
            var prize1 = new GoodsItem
            {
                GoodsId = goods1.Id,
                Num = 1,
                Title = "奖品1",
                ImgUrl = "http://test.com/p1.jpg",
                Stock = 1,
                SurplusStock = 1,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = 1,
                GoodsType = 1,
                PrizeCode = "PC001",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            var prize2 = new GoodsItem
            {
                GoodsId = goods2.Id,
                Num = 1,
                Title = "奖品2",
                ImgUrl = "http://test.com/p2.jpg",
                Stock = 1,
                SurplusStock = 1,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = 1,
                GoodsType = 1,
                PrizeCode = "PC002",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.AddRange(prize1, prize2);
            dbContext.SaveChanges();

            // Both should succeed (different goods)
            var request1 = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = prize1.Id,
                UserId = user.Id,
                Remark = "Config for goods1"
            };
            var request2 = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = prize2.Id,
                UserId = user.Id,
                Remark = "Config for goods2"
            };

            try
            {
                var result1 = service.CreateAsync(goods1.Id, request1).GetAwaiter().GetResult();
                var result2 = service.CreateAsync(goods2.Id, request2).GetAwaiter().GetResult();
                return result1 != null && result2 != null;
            }
            catch
            {
                return false;
            }
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    #endregion

    #region Property 8: Prize Data Immutability

    /// <summary>
    /// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
    /// For any designated prize configuration CREATE operation, the underlying prize's stock, 
    /// real_pro, and other probability-related fields SHALL remain unchanged.
    /// **Validates: Requirements 5.2**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool PrizeDataImmutability_CreateOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro, PositiveInt price)
    {
        var actualStock = Math.Max(1, stock.Get % 100 + 1);
        var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);
        var actualPrice = Math.Max(10, price.Get % 1000 + 10);

        var (dbContext, service) = CreateService();

        try
        {
            // Create user
            var user = new User
            {
                OpenId = $"user_{Guid.NewGuid()}",
                Uid = $"uid_{Guid.NewGuid()}",
                Nickname = "测试用户",
                HeadImg = "http://test.com/u.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Users.Add(user);
            dbContext.SaveChanges();

            // Create goods
            var goods = new Good
            {
                Title = "测试商品",
                ImgUrl = "http://test.com/g.jpg",
                ImgUrlDetail = "http://test.com/g_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Goods.Add(goods);
            dbContext.SaveChanges();

            // Create prize with specific values
            var prize = new GoodsItem
            {
                GoodsId = goods.Id,
                Num = 1,
                Title = "测试奖品",
                ImgUrl = "http://test.com/p.jpg",
                Stock = actualStock,
                SurplusStock = actualStock,
                Price = actualPrice,
                Money = actualPrice / 2,
                ScMoney = actualPrice / 3,
                RealPro = actualRealPro,
                GoodsType = 1,
                PrizeCode = "PC001",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.Add(prize);
            dbContext.SaveChanges();

            // Store original values
            var originalStock = prize.Stock;
            var originalSurplusStock = prize.SurplusStock;
            var originalRealPro = prize.RealPro;
            var originalPrice = prize.Price;
            var originalMoney = prize.Money;
            var originalScMoney = prize.ScMoney;

            // Create designated prize configuration
            var request = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = prize.Id,
                UserId = user.Id,
                Remark = "Test config"
            };
            service.CreateAsync(goods.Id, request).GetAwaiter().GetResult();

            // Reload prize from database
            dbContext.Entry(prize).Reload();

            // Verify all probability-related fields remain unchanged
            return prize.Stock == originalStock &&
                   prize.SurplusStock == originalSurplusStock &&
                   prize.RealPro == originalRealPro &&
                   prize.Price == originalPrice &&
                   prize.Money == originalMoney &&
                   prize.ScMoney == originalScMoney;
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
    /// For any designated prize configuration UPDATE operation, the underlying prize's stock, 
    /// real_pro, and other probability-related fields SHALL remain unchanged.
    /// **Validates: Requirements 5.2**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool PrizeDataImmutability_UpdateOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro)
    {
        var actualStock = Math.Max(1, stock.Get % 100 + 1);
        var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);

        var (dbContext, service) = CreateService();

        try
        {
            // Create user
            var user = new User
            {
                OpenId = $"user_{Guid.NewGuid()}",
                Uid = $"uid_{Guid.NewGuid()}",
                Nickname = "测试用户",
                HeadImg = "http://test.com/u.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Users.Add(user);
            dbContext.SaveChanges();

            // Create goods
            var goods = new Good
            {
                Title = "测试商品",
                ImgUrl = "http://test.com/g.jpg",
                ImgUrlDetail = "http://test.com/g_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Goods.Add(goods);
            dbContext.SaveChanges();

            // Create prize
            var prize = new GoodsItem
            {
                GoodsId = goods.Id,
                Num = 1,
                Title = "测试奖品",
                ImgUrl = "http://test.com/p.jpg",
                Stock = actualStock,
                SurplusStock = actualStock,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = actualRealPro,
                GoodsType = 1,
                PrizeCode = "PC001",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.Add(prize);
            dbContext.SaveChanges();

            // Create designated prize configuration
            var createRequest = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = prize.Id,
                UserId = user.Id,
                Remark = "Initial config"
            };
            var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();

            // Store original values
            var originalStock = prize.Stock;
            var originalSurplusStock = prize.SurplusStock;
            var originalRealPro = prize.RealPro;
            var originalPrice = prize.Price;

            // Update the configuration
            var updateRequest = new UpdateDesignatedPrizeRequest
            {
                IsActive = false,
                Remark = "Updated config"
            };
            service.UpdateAsync(config.Id, updateRequest).GetAwaiter().GetResult();

            // Reload prize from database
            dbContext.Entry(prize).Reload();

            // Verify all probability-related fields remain unchanged
            return prize.Stock == originalStock &&
                   prize.SurplusStock == originalSurplusStock &&
                   prize.RealPro == originalRealPro &&
                   prize.Price == originalPrice;
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
    /// For any designated prize configuration DELETE operation, the underlying prize's stock, 
    /// real_pro, and other probability-related fields SHALL remain unchanged.
    /// **Validates: Requirements 5.2**
    /// </summary>
    [Property(MaxTest = 100)]
    public bool PrizeDataImmutability_DeleteOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro)
    {
        var actualStock = Math.Max(1, stock.Get % 100 + 1);
        var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);

        var (dbContext, service) = CreateService();

        try
        {
            // Create user
            var user = new User
            {
                OpenId = $"user_{Guid.NewGuid()}",
                Uid = $"uid_{Guid.NewGuid()}",
                Nickname = "测试用户",
                HeadImg = "http://test.com/u.jpg",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Users.Add(user);
            dbContext.SaveChanges();

            // Create goods
            var goods = new Good
            {
                Title = "测试商品",
                ImgUrl = "http://test.com/g.jpg",
                ImgUrlDetail = "http://test.com/g_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Goods.Add(goods);
            dbContext.SaveChanges();

            // Create prize
            var prize = new GoodsItem
            {
                GoodsId = goods.Id,
                Num = 1,
                Title = "测试奖品",
                ImgUrl = "http://test.com/p.jpg",
                Stock = actualStock,
                SurplusStock = actualStock,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = actualRealPro,
                GoodsType = 1,
                PrizeCode = "PC001",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.Add(prize);
            dbContext.SaveChanges();

            // Create designated prize configuration
            var createRequest = new CreateDesignatedPrizeRequest
            {
                GoodsItemId = prize.Id,
                UserId = user.Id,
                Remark = "Config to delete"
            };
            var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();

            // Store original values
            var originalStock = prize.Stock;
            var originalSurplusStock = prize.SurplusStock;
            var originalRealPro = prize.RealPro;
            var originalPrice = prize.Price;

            // Delete the configuration
            service.DeleteAsync(config.Id).GetAwaiter().GetResult();

            // Reload prize from database
            dbContext.Entry(prize).Reload();

            // Verify all probability-related fields remain unchanged
            return prize.Stock == originalStock &&
                   prize.SurplusStock == originalSurplusStock &&
                   prize.RealPro == originalRealPro &&
                   prize.Price == originalPrice;
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    /// <summary>
    /// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
    /// Multiple CRUD operations on designated prize configurations should not affect prize data.
    /// **Validates: Requirements 5.2**
    /// </summary>
    [Property(MaxTest = 50)]
    public bool PrizeDataImmutability_MultipleCrudOperations_ShouldNotModifyPrizeFields(PositiveInt operationCount)
    {
        var actualOperationCount = Math.Max(3, operationCount.Get % 10 + 3);

        var (dbContext, service) = CreateService();

        try
        {
            // Create users
            var users = new List<User>();
            for (int i = 0; i < actualOperationCount; i++)
            {
                var user = new User
                {
                    OpenId = $"user{i}_{Guid.NewGuid()}",
                    Uid = $"uid{i}_{Guid.NewGuid()}",
                    Nickname = $"用户{i}",
                    HeadImg = $"http://test.com/u{i}.jpg",
                    CreatedAt = DateTime.Now,
                    UpdatedAt = DateTime.Now
                };
                users.Add(user);
            }
            dbContext.Users.AddRange(users);
            dbContext.SaveChanges();

            // Create goods
            var goods = new Good
            {
                Title = "测试商品",
                ImgUrl = "http://test.com/g.jpg",
                ImgUrlDetail = "http://test.com/g_detail.jpg",
                Price = 100,
                Type = 1,
                Status = 1,
                Stock = 100,
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.Goods.Add(goods);
            dbContext.SaveChanges();

            // Create prize
            var prize = new GoodsItem
            {
                GoodsId = goods.Id,
                Num = 1,
                Title = "测试奖品",
                ImgUrl = "http://test.com/p.jpg",
                Stock = 50,
                SurplusStock = 50,
                Price = 500,
                Money = 300,
                ScMoney = 250,
                RealPro = 25,
                GoodsType = 1,
                PrizeCode = "PC001",
                CreatedAt = DateTime.Now,
                UpdatedAt = DateTime.Now
            };
            dbContext.GoodsItems.Add(prize);
            dbContext.SaveChanges();

            // Store original values
            var originalStock = prize.Stock;
            var originalSurplusStock = prize.SurplusStock;
            var originalRealPro = prize.RealPro;
            var originalPrice = prize.Price;
            var originalMoney = prize.Money;
            var originalScMoney = prize.ScMoney;

            // Perform multiple CRUD operations
            for (int i = 0; i < actualOperationCount; i++)
            {
                // Create
                var createRequest = new CreateDesignatedPrizeRequest
                {
                    GoodsItemId = prize.Id,
                    UserId = users[i].Id,
                    Remark = $"Config {i}"
                };
                var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();

                // Update
                var updateRequest = new UpdateDesignatedPrizeRequest
                {
                    IsActive = i % 2 == 0,
                    Remark = $"Updated config {i}"
                };
                service.UpdateAsync(config.Id, updateRequest).GetAwaiter().GetResult();

                // Delete
                service.DeleteAsync(config.Id).GetAwaiter().GetResult();
            }

            // Reload prize from database
            dbContext.Entry(prize).Reload();

            // Verify all probability-related fields remain unchanged after all operations
            return prize.Stock == originalStock &&
                   prize.SurplusStock == originalSurplusStock &&
                   prize.RealPro == originalRealPro &&
                   prize.Price == originalPrice &&
                   prize.Money == originalMoney &&
                   prize.ScMoney == originalScMoney;
        }
        finally
        {
            dbContext.Dispose();
        }
    }

    #endregion

}
