using System.Text;
using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Stripe;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class BillingControllerTests
{
    [Fact]
    public async Task Checkout_uses_the_configured_price_and_stable_user_metadata()
    {
        var user = new ApplicationUser { Id = "user-1", Email = "person@example.test" };
        var users = TestHostFactory.CreateUserManager(user);
        users.Setup(item => item.GetRolesAsync(user)).ReturnsAsync(Array.Empty<string>());
        var gateway = new Mock<IStripeBillingGateway>();
        Stripe.Checkout.SessionCreateOptions? captured = null;
        gateway.Setup(item => item.CreateCheckoutAsync("sk_test_fake", It.IsAny<Stripe.Checkout.SessionCreateOptions>(), It.IsAny<CancellationToken>()))
            .Callback((string _, Stripe.Checkout.SessionCreateOptions options, CancellationToken _) => captured = options)
            .ReturnsAsync(new Stripe.Checkout.Session { Url = "https://checkout.stripe.test/session" });
        var controller = Controller(Configuration(), users, CreateRoleManager(), gateway);
        Authenticate(controller, user.Id);

        var action = await controller.Checkout(default);

        var result = Assert.IsType<OkObjectResult>(action.Result);
        Assert.Equal("https://checkout.stripe.test/session", Assert.IsType<BillingController.BillingRedirectDto>(result.Value).Url);
        Assert.NotNull(captured);
        Assert.Equal("price_fake", Assert.Single(captured.LineItems).Price);
        Assert.Equal(user.Id, captured.ClientReferenceId);
        Assert.Equal(user.Id, captured.Metadata["jobtracker_user_id"]);
        Assert.Equal(user.Id, captured.SubscriptionData.Metadata["jobtracker_user_id"]);
        Assert.Equal("https://example.test/settings?billing=success", captured.SuccessUrl);
    }

    [Fact]
    public async Task Product_identifier_cannot_enable_checkout_as_a_price()
    {
        var user = new ApplicationUser { Id = "user-1" };
        var users = TestHostFactory.CreateUserManager(user);
        users.Setup(item => item.GetRolesAsync(user)).ReturnsAsync(Array.Empty<string>());
        var configuration = Configuration(new Dictionary<string, string?> { ["Stripe:PricePremium"] = "prod_wrong_kind" });
        var controller = Controller(configuration, users, CreateRoleManager(), new Mock<IStripeBillingGateway>());
        Authenticate(controller, user.Id);

        var status = Assert.IsType<OkObjectResult>((await controller.Status(default)).Result);
        Assert.False(Assert.IsType<BillingController.BillingStatusDto>(status.Value).Enabled);
        Assert.IsType<ObjectResult>((await controller.Checkout(default)).Result);
    }

    [Fact]
    public async Task Signed_subscription_lifecycle_grants_then_revokes_on_expiry_idempotently()
    {
        var user = new ApplicationUser
        {
            Id = "user-1",
            AiEnabled = true,
            ProfileCvText = "non-AI profile data must survive",
        };
        var users = TestHostFactory.CreateUserManager(user);
        users.Setup(item => item.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
        var hasPremium = false;
        users.Setup(item => item.IsInRoleAsync(user, "Premium")).ReturnsAsync(() => hasPremium);
        users.Setup(item => item.AddToRoleAsync(user, "Premium"))
            .Callback(() => hasPremium = true)
            .ReturnsAsync(IdentityResult.Success);
        users.Setup(item => item.RemoveFromRoleAsync(user, "Premium"))
            .Callback(() => hasPremium = false)
            .ReturnsAsync(IdentityResult.Success);

        var roles = CreateRoleManager();
        roles.Setup(item => item.RoleExistsAsync("Premium")).ReturnsAsync(true);
        var webhookEvent = new Event
        {
            Type = EventTypes.CustomerSubscriptionUpdated,
            Created = new DateTime(2026, 8, 15, 10, 0, 0, DateTimeKind.Utc),
            Data = new EventData { Object = new Subscription { Id = "sub_lifecycle" } },
        };
        var gateway = new Mock<IStripeBillingGateway>();
        gateway.Setup(item => item.ConstructEvent(It.IsAny<string>(), "signed", "whsec_fake")).Returns(webhookEvent);
        gateway.SetupSequence(item => item.GetSubscriptionAsync("sk_test_fake", "sub_lifecycle", It.IsAny<CancellationToken>()))
            .ReturnsAsync(Subscription("active"))
            .ReturnsAsync(Subscription("past_due"))
            .ReturnsAsync(Subscription("canceled"));
        var controller = Controller(Configuration(), users, roles, gateway);

        SetWebhookRequest(controller);
        Assert.IsType<OkResult>(await controller.Webhook(default));
        Assert.True(hasPremium);
        Assert.Equal("active", user.StripeSubscriptionStatus);

        SetWebhookRequest(controller);
        Assert.IsType<OkResult>(await controller.Webhook(default));
        Assert.False(hasPremium);
        Assert.Equal("past_due", user.StripeSubscriptionStatus);
        Assert.Equal("non-AI profile data must survive", user.ProfileCvText);

        SetWebhookRequest(controller);
        Assert.IsType<OkResult>(await controller.Webhook(default));
        Assert.False(hasPremium);
        Assert.Equal("canceled", user.StripeSubscriptionStatus);
        users.Verify(item => item.AddToRoleAsync(user, "Premium"), Times.Once);
        users.Verify(item => item.RemoveFromRoleAsync(user, "Premium"), Times.Once);
        users.Verify(item => item.UpdateAsync(user), Times.Exactly(3));
    }

    [Fact]
    public async Task Webhook_rejects_an_invalid_Stripe_signature()
    {
        var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Stripe:SecretKey"] = "sk_test_fake",
            ["Stripe:PricePremium"] = "price_fake",
            ["Stripe:WebhookSecret"] = "whsec_fake",
            ["App:PublicBaseUrl"] = "https://example.test",
        }).Build();

        var userStore = new Mock<IUserStore<ApplicationUser>>();
        var users = new Mock<UserManager<ApplicationUser>>(
            userStore.Object, Options.Create(new IdentityOptions()), new PasswordHasher<ApplicationUser>(),
            Array.Empty<IUserValidator<ApplicationUser>>(), Array.Empty<IPasswordValidator<ApplicationUser>>(),
            new UpperInvariantLookupNormalizer(), new IdentityErrorDescriber(), null!, NullLogger<UserManager<ApplicationUser>>.Instance);
        var roleStore = new Mock<IRoleStore<IdentityRole>>();
        var roles = new Mock<RoleManager<IdentityRole>>(
            roleStore.Object, Array.Empty<IRoleValidator<IdentityRole>>(), new UpperInvariantLookupNormalizer(),
            new IdentityErrorDescriber(), NullLogger<RoleManager<IdentityRole>>.Instance);

        var controller = new BillingController(configuration, users.Object, roles.Object, NullLogger<BillingController>.Instance)
        {
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
        };
        controller.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
        controller.Request.Headers["Stripe-Signature"] = "invalid";

        var result = await controller.Webhook(CancellationToken.None);

        Assert.IsType<BadRequestObjectResult>(result);
    }

    private static IConfiguration Configuration(Dictionary<string, string?>? overrides = null)
    {
        var values = new Dictionary<string, string?>
        {
            ["Stripe:SecretKey"] = "sk_test_fake",
            ["Stripe:PricePremium"] = "price_fake",
            ["Stripe:WebhookSecret"] = "whsec_fake",
            ["App:PublicBaseUrl"] = "https://example.test",
        };
        if (overrides is not null)
            foreach (var (key, value) in overrides) values[key] = value;
        return new ConfigurationBuilder().AddInMemoryCollection(values).Build();
    }

    private static Mock<RoleManager<IdentityRole>> CreateRoleManager()
    {
        var store = new Mock<IRoleStore<IdentityRole>>();
        return new Mock<RoleManager<IdentityRole>>(
            store.Object,
            Array.Empty<IRoleValidator<IdentityRole>>(),
            new UpperInvariantLookupNormalizer(),
            new IdentityErrorDescriber(),
            NullLogger<RoleManager<IdentityRole>>.Instance);
    }

    private static BillingController Controller(
        IConfiguration configuration,
        Mock<UserManager<ApplicationUser>> users,
        Mock<RoleManager<IdentityRole>> roles,
        Mock<IStripeBillingGateway> gateway)
        => new(configuration, users.Object, roles.Object, NullLogger<BillingController>.Instance, stripe: gateway.Object)
        {
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() },
        };

    private static void Authenticate(BillingController controller, string userId)
        => controller.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "local"));

    private static void SetWebhookRequest(BillingController controller)
    {
        controller.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
        controller.Request.Headers["Stripe-Signature"] = "signed";
    }

    private static Subscription Subscription(string status) => new()
    {
        Id = "sub_lifecycle",
        CustomerId = "cus_lifecycle",
        Status = status,
        Metadata = new Dictionary<string, string> { ["jobtracker_user_id"] = "user-1" },
        Items = new StripeList<SubscriptionItem>
        {
            Data = new List<SubscriptionItem>
            {
                new() { Price = new Price { Id = "price_fake" } },
            },
        },
    };
}
