using System.Security.Claims; using AutoMapper; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using PowderCoating.Application.DTOs.GiftCertificate; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Infrastructure.Data; using PowderCoating.Infrastructure.Repositories; using PowderCoating.Web.Controllers; namespace PowderCoating.UnitTests; public class GiftCertificatesControllerTests { // ── Index — lazy expiration ─────────────────────────────────────────── [Fact] public async Task Index_LazilySetsExpiredStatus_ForActiveCertsPastExpiryDate() { await using var context = CreateContext(); context.GiftCertificates.Add(new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-2603-0001", OriginalAmount = 100m, RedeemedAmount = 0, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow.AddMonths(-2), ExpiryDate = DateTime.UtcNow.AddDays(-1) // past expiry }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Index(null, null); var view = Assert.IsType(result); var dtos = Assert.IsAssignableFrom>(view.Model); Assert.Single(dtos); Assert.Equal(GiftCertificateStatus.Expired, dtos[0].Status); var dbCert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Equal(GiftCertificateStatus.Expired, dbCert.Status); } [Fact] public async Task Index_DoesNotExpire_ActiveCertsWithFutureExpiryDate() { await using var context = CreateContext(); context.GiftCertificates.Add(new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-2604-0001", OriginalAmount = 50m, RedeemedAmount = 0, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow, ExpiryDate = DateTime.UtcNow.AddDays(30) // future expiry }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); await controller.Index(null, null); var dbCert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Equal(GiftCertificateStatus.Active, dbCert.Status); } // ── Index — filtering ───────────────────────────────────────────────── [Fact] public async Task Index_StatusFilter_ReturnsOnlyMatchingStatus() { await using var context = CreateContext(); context.GiftCertificates.AddRange( new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-2604-0001", OriginalAmount = 100m, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow }, new GiftCertificate { Id = 2, CompanyId = 1, CertificateCode = "GC-2604-0002", OriginalAmount = 200m, Status = GiftCertificateStatus.Voided, IssueDate = DateTime.UtcNow }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Index(null, nameof(GiftCertificateStatus.Active)); var view = Assert.IsType(result); var dtos = Assert.IsAssignableFrom>(view.Model); var dto = Assert.Single(dtos); Assert.Equal(GiftCertificateStatus.Active, dto.Status); } [Fact] public async Task Index_SearchTerm_FiltersByCertificateCode() { await using var context = CreateContext(); context.GiftCertificates.AddRange( new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-2604-0001", OriginalAmount = 100m, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow }, new GiftCertificate { Id = 2, CompanyId = 1, CertificateCode = "GC-2604-0002", OriginalAmount = 100m, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Index("0001", null); var view = Assert.IsType(result); var dtos = Assert.IsAssignableFrom>(view.Model); var dto = Assert.Single(dtos); Assert.Equal("GC-2604-0001", dto.CertificateCode); } [Fact] public async Task Index_ViewBag_TotalValueIncludesOnlyActiveAndPartiallyRedeemed() { await using var context = CreateContext(); context.GiftCertificates.AddRange( new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-A", OriginalAmount = 100m, RedeemedAmount = 0, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow }, new GiftCertificate { Id = 2, CompanyId = 1, CertificateCode = "GC-P", OriginalAmount = 80m, RedeemedAmount = 30m, Status = GiftCertificateStatus.PartiallyRedeemed, IssueDate = DateTime.UtcNow }, new GiftCertificate { Id = 3, CompanyId = 1, CertificateCode = "GC-V", OriginalAmount = 200m, RedeemedAmount = 200m, Status = GiftCertificateStatus.FullyRedeemed, IssueDate = DateTime.UtcNow }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); await controller.Index(null, null); // Active: RemainingBalance=100, PartiallyRedeemed: 80-30=50, FullyRedeemed: excluded Assert.Equal(150m, controller.ViewBag.TotalValue); Assert.Equal(2, controller.ViewBag.TotalActive); } // ── Create — success paths ──────────────────────────────────────────── [Fact] public async Task Create_WithSoldReason_SetsPurchasePriceAndGeneratesCode() { await using var context = CreateContext(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Create(new CreateGiftCertificateDto { Amount = 100m, IssuedReason = GiftCertificateIssuedReason.Sold, PurchasePrice = 80m, PurchasingCustomerId = null, RecipientName = "Jane Doe" }); Assert.IsType(result); var cert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Equal(80m, cert.PurchasePrice); Assert.Equal(GiftCertificateStatus.Active, cert.Status); Assert.Matches(@"^GC-\d{4}-\d{4}$", cert.CertificateCode); Assert.EndsWith("-0001", cert.CertificateCode); } [Fact] public async Task Create_WithNonSoldReason_NullsPurchasePriceAndPurchasingCustomerId() { await using var context = CreateContext(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Create(new CreateGiftCertificateDto { Amount = 50m, IssuedReason = GiftCertificateIssuedReason.Promotional, PurchasePrice = 50m, // Provided but should be nulled PurchasingCustomerId = 7 // Provided but should be nulled }); Assert.IsType(result); var cert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Null(cert.PurchasePrice); Assert.Null(cert.PurchasingCustomerId); } // ── Void ───────────────────────────────────────────────────────────── [Fact] public async Task Void_ActiveCert_ChangesStatusToVoidedAndRedirectsToIndex() { await using var context = CreateContext(); context.GiftCertificates.Add(new GiftCertificate { Id = 1, CompanyId = 1, CertificateCode = "GC-2604-0001", OriginalAmount = 100m, RedeemedAmount = 0, Status = GiftCertificateStatus.Active, IssueDate = DateTime.UtcNow }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Void(id: 1); var redirect = Assert.IsType(result); Assert.Equal("Index", redirect.ActionName); var dbCert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Equal(GiftCertificateStatus.Voided, dbCert.Status); } [Fact] public async Task Void_FullyRedeemedCert_BlocksWithTempDataErrorAndRedirectsToDetails() { await using var context = CreateContext(); context.GiftCertificates.Add(new GiftCertificate { Id = 2, CompanyId = 1, CertificateCode = "GC-2604-0002", OriginalAmount = 100m, RedeemedAmount = 100m, Status = GiftCertificateStatus.FullyRedeemed, IssueDate = DateTime.UtcNow }); await context.SaveChangesAsync(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Void(id: 2); var redirect = Assert.IsType(result); Assert.Equal("Details", redirect.ActionName); Assert.Contains("fully redeemed", controller.TempData["Error"]?.ToString()); var dbCert = await context.GiftCertificates.IgnoreQueryFilters().SingleAsync(); Assert.Equal(GiftCertificateStatus.FullyRedeemed, dbCert.Status); // unchanged } [Fact] public async Task Void_NonExistentCert_ReturnsNotFound() { await using var context = CreateContext(); var controller = CreateController(context, CreateUser(companyId: 1)); var result = await controller.Void(id: 999); Assert.IsType(result); } // ── Helpers ─────────────────────────────────────────────────────────── private static GiftCertificatesController CreateController( ApplicationDbContext context, ApplicationUser? currentUser) { var uow = new UnitOfWork(context); var userManager = CreateUserManagerMock(); userManager .Setup(m => m.GetUserAsync(It.IsAny())) .ReturnsAsync(currentUser); var httpContext = new DefaultHttpContext(); var controller = new GiftCertificatesController( uow, Mock.Of(), Mock.Of>(), userManager.Object, Mock.Of()); controller.ControllerContext = new ControllerContext { HttpContext = httpContext }; controller.TempData = new TempDataDictionary(httpContext, Mock.Of()); return controller; } private static ApplicationUser CreateUser(int companyId) => new() { Id = "test-user", CompanyId = companyId, UserName = "test@example.com", Email = "test@example.com", FirstName = "Test", LastName = "User" }; private static Mock> CreateUserManagerMock() { var store = new Mock>(); return new Mock>( store.Object, null!, null!, null!, null!, null!, null!, null!, null!); } private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; // Use a SuperAdmin user (no CompanyId claim) so that IsPlatformAdmin = true and the // global query filter evaluates to !IsDeleted only — not !IsDeleted && CompanyId == null, // which would filter out every row. DefaultHttpContext.Session throws, so we mock // HttpContext itself and stub Session.TryGetValue to return false (no impersonation). var identity = new ClaimsIdentity( new[] { new Claim(ClaimTypes.Role, "SuperAdmin") }, "Test"); var principal = new ClaimsPrincipal(identity); byte[]? noBytes = null; var sessionMock = new Mock(); sessionMock.Setup(s => s.TryGetValue(It.IsAny(), out noBytes)).Returns(false); var httpContextMock = new Mock(); httpContextMock.SetupGet(c => c.User).Returns(principal); httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object); var accessor = new Mock(); accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); return new ApplicationDbContext(options, accessor.Object, null!); } }