using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using PowderCoating.Application.DTOs.Registration;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Shared.Constants;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Infrastructure.Repositories;
using System.Text.Json;
using PowderCoating.Web.Controllers;
using Xunit;

namespace PowderCoating.UnitTests;

public class RegistrationControllerTests
{
    [Fact]
    public async Task PaymentSuccess_WhenStripeSessionIsNotPaid_DoesNotBurnPendingSession()
    {
        await using var context = CreateContext();
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-1", "owner@example.com"));
        await context.SaveChangesAsync();

        var stripeService = new Mock<IStripeService>();
        stripeService.Setup(x => x.IsRegistrationCheckoutPaidAsync("sess_unpaid")).ReturnsAsync(false);

        var controller = CreateController(context, stripeService: stripeService);

        var result = await controller.PaymentSuccess("sess_unpaid", "token-1");

        var redirect = Assert.IsType<Microsoft.AspNetCore.Mvc.RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.False((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
        Assert.Contains("couldn't verify a completed payment", controller.TempData["Error"]?.ToString());
    }

    [Fact]
    public async Task PaymentSuccess_WhenUserCreationFails_ReleasesPendingSessionAndDeletesCompany()
    {
        await using var context = CreateContext();
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-2", "owner2@example.com"));
        await context.SaveChangesAsync();

        var userManager = CreateUserManagerMock();
        userManager.Setup(x => x.FindByEmailAsync("owner2@example.com")).ReturnsAsync((ApplicationUser?)null);
        userManager.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
            .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "boom" }));

        var stripeService = new Mock<IStripeService>();
        stripeService.Setup(x => x.IsRegistrationCheckoutPaidAsync("sess_paid")).ReturnsAsync(true);

        var controller = CreateController(context, userManager, stripeService: stripeService);

        var result = await controller.PaymentSuccess("sess_paid", "token-2");

        var redirect = Assert.IsType<Microsoft.AspNetCore.Mvc.RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.False((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
        Assert.Empty(context.Companies);
        Assert.Contains("Please try the success link again", controller.TempData["Error"]?.ToString());
    }

    [Fact]
    public async Task PaymentSuccess_WhenSessionAlreadyCompletedAndUserExists_SignsUserInAndRedirectsToWelcome()
    {
        await using var context = CreateContext();
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-3", "owner3@example.com", isCompleted: true));
        await context.SaveChangesAsync();

        var existingUser = new ApplicationUser
        {
            Id = "user-3",
            Email = "owner3@example.com",
            UserName = "owner3@example.com",
            FirstName = "Terry",
            LastName = "Tenant",
            CompanyId = 3
        };

        var userManager = CreateUserManagerMock();
        userManager.Setup(x => x.FindByEmailAsync("owner3@example.com")).ReturnsAsync(existingUser);
        userManager.Setup(x => x.UpdateAsync(existingUser)).ReturnsAsync(IdentityResult.Success);

        var signInManager = CreateSignInManagerMock(userManager.Object);
        signInManager.Setup(x => x.SignInAsync(existingUser, false, null)).Returns(Task.CompletedTask).Verifiable();

        var controller = CreateController(context, userManager, signInManager.Object);

        var result = await controller.PaymentSuccess("sess_complete", "token-3");

        var redirect = Assert.IsType<Microsoft.AspNetCore.Mvc.RedirectToActionResult>(result);
        Assert.Equal("Welcome", redirect.ActionName);
        signInManager.Verify(x => x.SignInAsync(existingUser, false, null), Times.Once);
        Assert.True((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
    }

    [Fact]
    public async Task Create_WhenEmailAlreadyExists_ReturnsIndexWithModelError()
    {
        await using var context = CreateContext();
        SeedPlanConfig(context, plan: 1);
        await context.SaveChangesAsync();

        var existingUser = new ApplicationUser
        {
            Id = "existing",
            Email = "owner@example.com",
            UserName = "owner@example.com",
            FirstName = "Existing",
            LastName = "User"
        };

        var userManager = CreateUserManagerMock();
        userManager.Setup(x => x.FindByEmailAsync("owner@example.com")).ReturnsAsync(existingUser);

        var controller = CreateController(context, userManager: userManager);

        var model = new RegisterCompanyDto
        {
            CompanyName = "Dup Co",
            CompanyPhone = "555-0100",
            FirstName = "Pat",
            LastName = "Owner",
            Email = "owner@example.com",
            Plan = 1
        };

        var result = await controller.Create(model);

        var view = Assert.IsType<ViewResult>(result);
        Assert.Equal("Index", view.ViewName);
        Assert.False(controller.ModelState.IsValid);
        Assert.Contains(controller.ModelState["Email"]!.Errors, e => e.ErrorMessage.Contains("already exists"));
    }

    [Fact]
    public async Task Create_WhenRegistrationIsClosed_ReturnsIndexWithTempDataError()
    {
        await using var context = CreateContext();
        SeedPlanConfig(context, plan: 1);
        context.Companies.Add(new Company
        {
            Id = 1,
            CompanyId = 1,
            CompanyName = "Existing Company",
            CompanyCode = "EXC",
            PrimaryContactName = "Owner",
            PrimaryContactEmail = "existing@example.com",
            IsActive = true
        });
        await context.SaveChangesAsync();

        var platformSettings = new Mock<IPlatformSettingsService>();
        platformSettings
            .Setup(x => x.GetAsync(PlatformSettingKeys.MaxTenants))
            .ReturnsAsync("1");
        platformSettings
            .Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.MaxTenants)))
            .ReturnsAsync((string?)null);

        var controller = CreateController(context, platformSettings: platformSettings);

        var model = new RegisterCompanyDto
        {
            CompanyName = "New Co",
            CompanyPhone = "555-0100",
            FirstName = "Pat",
            LastName = "Owner",
            Email = "owner@example.com",
            Plan = 1
        };

        var result = await controller.Create(model);

        var view = Assert.IsType<ViewResult>(result);
        Assert.Equal("Index", view.ViewName);
        Assert.Equal("Registration is currently closed. Please contact us for more information.", controller.TempData["Error"]);
        Assert.False((bool)controller.ViewBag.RegistrationOpen);
    }

    [Fact]
    public async Task Create_WhenTrialsDisabledAndStripeCheckoutStarts_RedirectsAndPersistsPendingSession()
    {
        await using var context = CreateContext();
        SeedPlanConfig(context, plan: 1);
        await context.SaveChangesAsync();

        var platformSettings = new Mock<IPlatformSettingsService>();
        platformSettings.Setup(x => x.GetAsync(PlatformSettingKeys.TrialsEnabled)).ReturnsAsync("false");
        platformSettings.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.TrialsEnabled))).ReturnsAsync((string?)null);

        var stripeService = new Mock<IStripeService>();
        stripeService
            .Setup(x => x.CreateRegistrationCheckoutSessionAsync(
                1, false, "paid@example.com", "Paid Co", It.IsAny<string>(), It.IsAny<string>()))
            .ReturnsAsync("https://checkout.example/session");

        var controller = CreateController(
            context,
            stripeService: stripeService,
            platformSettings: platformSettings);

        var model = new RegisterCompanyDto
        {
            CompanyName = "Paid Co",
            CompanyPhone = "555-0100",
            FirstName = "Pat",
            LastName = "Owner",
            Email = "paid@example.com",
            Plan = 1
        };

        var result = await controller.Create(model);

        var redirect = Assert.IsType<RedirectResult>(result);
        Assert.Equal("https://checkout.example/session", redirect.Url);

        var pending = await context.PendingRegistrationSessions.SingleAsync();
        Assert.Equal("Paid Co", pending.CompanyName);
        Assert.Equal("paid@example.com", pending.Email);
        Assert.False(pending.IsCompleted);
    }

    [Fact]
    public async Task Create_WhenStripeConfigFails_DoesNotPersistPendingSession()
    {
        await using var context = CreateContext();
        SeedPlanConfig(context, plan: 1);
        await context.SaveChangesAsync();

        var platformSettings = new Mock<IPlatformSettingsService>();
        platformSettings.Setup(x => x.GetAsync(PlatformSettingKeys.TrialsEnabled)).ReturnsAsync("false");
        platformSettings.Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.TrialsEnabled))).ReturnsAsync((string?)null);

        var stripeService = new Mock<IStripeService>();
        stripeService
            .Setup(x => x.CreateRegistrationCheckoutSessionAsync(
                It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
            .ThrowsAsync(new InvalidOperationException("Stripe prices are not configured."));

        var controller = CreateController(
            context,
            stripeService: stripeService,
            platformSettings: platformSettings);

        var model = new RegisterCompanyDto
        {
            CompanyName = "Paid Co",
            CompanyPhone = "555-0100",
            FirstName = "Pat",
            LastName = "Owner",
            Email = "paid@example.com",
            Plan = 1
        };

        var result = await controller.Create(model);

        var view = Assert.IsType<ViewResult>(result);
        Assert.Equal("Index", view.ViewName);
        Assert.Empty(context.PendingRegistrationSessions);
        Assert.Contains(controller.ModelState[string.Empty]!.Errors, e => e.ErrorMessage.Contains("Stripe prices are not configured."));
    }

    [Fact]
    public async Task PaymentSuccess_WhenRegistrationClosedAfterPayment_ReleasesPendingSessionWithoutCreatingCompany()
    {
        await using var context = CreateContext();
        SeedPlanConfig(context, plan: 1);
        context.Companies.Add(new Company
        {
            Id = 1,
            CompanyId = 1,
            CompanyName = "At Capacity",
            CompanyCode = "CAP",
            PrimaryContactName = "Owner",
            PrimaryContactEmail = "capacity@example.com",
            IsActive = true
        });
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-closed", "closed@example.com"));
        await context.SaveChangesAsync();

        var platformSettings = new Mock<IPlatformSettingsService>();
        platformSettings
            .Setup(x => x.GetAsync(PlatformSettingKeys.MaxTenants))
            .ReturnsAsync("1");
        platformSettings
            .Setup(x => x.GetAsync(It.Is<string>(key => key != PlatformSettingKeys.MaxTenants)))
            .ReturnsAsync((string?)null);

        var userManager = CreateUserManagerMock();
        userManager.Setup(x => x.FindByEmailAsync("closed@example.com")).ReturnsAsync((ApplicationUser?)null);

        var stripeService = new Mock<IStripeService>();
        stripeService.Setup(x => x.IsRegistrationCheckoutPaidAsync("sess_paid")).ReturnsAsync(true);

        var controller = CreateController(
            context,
            userManager: userManager,
            stripeService: stripeService,
            platformSettings: platformSettings);

        var result = await controller.PaymentSuccess("sess_paid", "token-closed");

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.Equal("Registration is currently closed. Your payment has been received but no account was created. Please contact support.", controller.TempData["Error"]);
        Assert.False((await context.PendingRegistrationSessions.SingleAsync()).IsCompleted);
        Assert.Single(context.Companies);
    }

    [Fact]
    public async Task PaymentSuccess_WhenSessionIdMissing_RedirectsToIndex()
    {
        await using var context = CreateContext();
        var controller = CreateController(context);

        var result = await controller.PaymentSuccess(null, "token");

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
    }

    [Fact]
    public async Task PaymentSuccess_WhenRegistrationTokenMissing_SetsExpiredError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context);

        var result = await controller.PaymentSuccess("sess_123", null);

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.Equal("Your registration session has expired. Please fill in your details again.", controller.TempData["Error"]);
    }

    [Fact]
    public async Task PaymentSuccess_WhenPendingSessionMissing_SetsNotFoundError()
    {
        await using var context = CreateContext();
        var controller = CreateController(context);

        var result = await controller.PaymentSuccess("sess_123", "missing-token");

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.Equal("Your registration session was not found. Please fill in your details again.", controller.TempData["Error"]);
    }

    [Fact]
    public async Task PaymentSuccess_WhenSessionAlreadyCompletedButUserMissing_ShowsSupportError()
    {
        await using var context = CreateContext();
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-missing-user", "retry@example.com", isCompleted: true));
        await context.SaveChangesAsync();

        var userManager = CreateUserManagerMock();
        userManager.Setup(x => x.FindByEmailAsync("retry@example.com")).ReturnsAsync((ApplicationUser?)null);

        var controller = CreateController(context, userManager: userManager);

        var result = await controller.PaymentSuccess("sess_done", "token-missing-user");

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);
        Assert.Contains("couldn't finish signing you in", controller.TempData["Error"]?.ToString());
    }

    [Fact]
    public async Task PaymentCancelled_WhenPendingSessionExists_PrefillsTempDataAndDeletesSession()
    {
        await using var context = CreateContext();
        context.PendingRegistrationSessions.Add(CreatePendingSession("token-cancel", "cancel@example.com"));
        await context.SaveChangesAsync();

        var controller = CreateController(context);

        var result = await controller.PaymentCancelled("token-cancel");

        var redirect = Assert.IsType<RedirectToActionResult>(result);
        Assert.Equal("Index", redirect.ActionName);

        var json = Assert.IsType<string>(controller.TempData["PendingRegistrationJson"]);
        var model = JsonSerializer.Deserialize<RegisterCompanyDto>(json);
        Assert.NotNull(model);
        Assert.Equal("Retry Co", model!.CompanyName);
        Assert.Equal("cancel@example.com", model.Email);
        Assert.Empty(context.PendingRegistrationSessions);
    }

    private static RegistrationController CreateController(
        ApplicationDbContext context,
        Mock<UserManager<ApplicationUser>>? userManager = null,
        SignInManager<ApplicationUser>? signInManager = null,
        Mock<IStripeService>? stripeService = null,
        Mock<IPlatformSettingsService>? platformSettings = null)
    {
        var unitOfWork = new UnitOfWork(context);
        var userManagerMock = userManager ?? CreateUserManagerMock();
        var signInManagerInstance = signInManager ?? CreateSignInManagerMock(userManagerMock.Object).Object;

        var platformSettingsMock = platformSettings ?? new Mock<IPlatformSettingsService>();
        if (platformSettings is null)
        {
            platformSettingsMock.Setup(x => x.GetAsync(It.IsAny<string>())).ReturnsAsync((string?)null);
        }

        var controller = new RegistrationController(
            unitOfWork,
            context,
            userManagerMock.Object,
            signInManagerInstance,
            Mock.Of<ISeedDataService>(),
            Mock.Of<IAdminNotificationService>(),
            Mock.Of<IInAppNotificationService>(),
            platformSettingsMock.Object,
            (stripeService ?? new Mock<IStripeService>()).Object,
            Mock.Of<IEmailService>(),
            Mock.Of<ILogger<RegistrationController>>());

        var httpContext = new DefaultHttpContext();
        controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext
        {
            HttpContext = httpContext
        };
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(x => x.Action(It.IsAny<UrlActionContext>()))
            .Returns<UrlActionContext>(ctx => $"https://example.test/{ctx.Action}");
        controller.Url = urlHelper.Object;
        controller.TempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());

        return controller;
    }

    private static Mock<UserManager<ApplicationUser>> CreateUserManagerMock()
    {
        var store = new Mock<IUserStore<ApplicationUser>>();
        return new Mock<UserManager<ApplicationUser>>(
            store.Object,
            null!,
            null!,
            null!,
            null!,
            null!,
            null!,
            null!,
            null!);
    }

    private static Mock<SignInManager<ApplicationUser>> CreateSignInManagerMock(UserManager<ApplicationUser> userManager)
    {
        var contextAccessor = new Mock<IHttpContextAccessor>();
        contextAccessor.Setup(x => x.HttpContext).Returns(new DefaultHttpContext());

        var claimsFactory = new Mock<IUserClaimsPrincipalFactory<ApplicationUser>>();

        return new Mock<SignInManager<ApplicationUser>>(
            userManager,
            contextAccessor.Object,
            claimsFactory.Object,
            null!,
            null!,
            null!,
            null!);
    }

    private static ApplicationDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        return new ApplicationDbContext(options);
    }

    private static void SeedPlanConfig(ApplicationDbContext context, int plan)
    {
        context.SubscriptionPlanConfigs.Add(new SubscriptionPlanConfig
        {
            Id = plan,
            CompanyId = 0,
            Plan = plan,
            DisplayName = $"Plan {plan}",
            SortOrder = plan,
            IsActive = true
        });
    }

    private static PendingRegistrationSession CreatePendingSession(string token, string email, bool isCompleted = false)
    {
        return new PendingRegistrationSession
        {
            Token = token,
            CompanyName = "Retry Co",
            CompanyPhone = "555-0100",
            FirstName = "Pat",
            LastName = "Owner",
            Email = email,
            Plan = 1,
            IsAnnual = false,
            IsCompleted = isCompleted,
            CreatedAt = DateTime.UtcNow
        };
    }
}
