package com.yumu.noveltranslator.application.service;
import com.yumu.noveltranslator.exception.BusinessException;
import com.yumu.noveltranslator.port.dto.subscription.SubscriptionStatusResponse;
import com.yumu.noveltranslator.util.JwtUtils;
import com.yumu.noveltranslator.port.dto.subscription.PortalSessionResponse;
import com.yumu.noveltranslator.port.dto.subscription.CheckoutSessionRequest;
import com.yumu.noveltranslator.port.dto.subscription.CheckoutSessionResponse;
import com.yumu.noveltranslator.application.service.SubscriptionApplicationService;
import com.yumu.noveltranslator.port.out.TokenRevocationPort;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import com.stripe.Stripe;
import com.stripe.model.Event;
import com.stripe.model.EventDataObjectDeserializer;
import com.stripe.model.Subscription;
import com.stripe.model.SubscriptionItemCollection;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionCreateParams;
import com.yumu.noveltranslator.port.dto.common.*;
import com.yumu.noveltranslator.port.dto.collab.*;
import com.yumu.noveltranslator.port.dto.entity.*;
import com.yumu.noveltranslator.port.dto.translation.*;
import com.yumu.noveltranslator.port.dto.subscription.*;
import com.yumu.noveltranslator.port.dto.auth.*;
import com.yumu.noveltranslator.domain.model.StripeCustomer;
import com.yumu.noveltranslator.domain.model.StripeSubscription;
import com.yumu.noveltranslator.domain.model.User;
import com.yumu.noveltranslator.domain.model.UserPlanHistory;
import com.yumu.noveltranslator.port.out.BillingRepositoryPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import com.yumu.noveltranslator.properties.StripeProperties;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SubscriptionServiceTest {

    @Mock
    private StripeProperties stripeProperties;

    @Mock
    private BillingRepositoryPort billingPort;

    @Mock
    private UserRepositoryPort userRepositoryPort;

    @Mock
    private StringRedisTemplate stringRedisTemplate;

    @Mock
    private TokenRevocationPort tokenRevocationPort;

    @Mock
    private com.yumu.noveltranslator.port.out.PaymentPort paymentPort;

    @Mock
    private org.springframework.transaction.PlatformTransactionManager transactionManager;

    private SubscriptionApplicationService subscriptionService;

    @BeforeAll
    static void initMybatisPlusCache() {
        Configuration configuration = new Configuration();
        MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
        assistant.setCurrentNamespace("test");
        TableInfoHelper.initTableInfo(assistant, StripeSubscription.class);
    }

    @BeforeEach
    void setUp() {
        subscriptionService = new SubscriptionApplicationService(
                stripeProperties, billingPort, userRepositoryPort,
                stringRedisTemplate, tokenRevocationPort,
                paymentPort, transactionManager);
    }

    // ==================== getSubscriptionStatus ====================

    @Nested
    @DisplayName("getSubscriptionStatus")
    class GetSubscriptionStatusTests {

        @Test
        void 无订阅返回FREE状态() {
            when(billingPort.findSubscriptionsByUserId(1L)).thenReturn(List.of());

            SubscriptionStatusResponse response = subscriptionService.getSubscriptionStatus(1L);

            assertNotNull(response);
            assertEquals("FREE", response.getPlan());
            assertEquals("none", response.getStatus());
            assertNull(response.getPeriodEnd());
            assertFalse(response.getCancelAtPeriodEnd());
        }

        @Test
        void 有活跃订阅返回正确状态() {
            StripeSubscription sub = new StripeSubscription();
            sub.setId(1L);
            sub.setUserId(1L);
            sub.setPlan("PRO");
            sub.setStatus("active");
            sub.setCurrentPeriodEnd(LocalDateTime.of(2026, 5, 1, 0, 0));
            sub.setCancelAtPeriodEnd(false);

            when(billingPort.findSubscriptionsByUserId(1L)).thenReturn(List.of(sub));

            SubscriptionStatusResponse response = subscriptionService.getSubscriptionStatus(1L);

            assertNotNull(response);
            assertEquals("PRO", response.getPlan());
            assertEquals("active", response.getStatus());
            assertEquals(LocalDateTime.of(2026, 5, 1, 0, 0), response.getPeriodEnd());
            assertFalse(response.getCancelAtPeriodEnd());
        }

        @Test
        void 已标记取消返回true() {
            StripeSubscription sub = new StripeSubscription();
            sub.setId(1L);
            sub.setUserId(1L);
            sub.setPlan("MAX");
            sub.setStatus("active");
            sub.setCurrentPeriodEnd(LocalDateTime.of(2026, 6, 1, 0, 0));
            sub.setCancelAtPeriodEnd(true);

            when(billingPort.findSubscriptionsByUserId(1L)).thenReturn(List.of(sub));

            SubscriptionStatusResponse response = subscriptionService.getSubscriptionStatus(1L);

            assertTrue(response.getCancelAtPeriodEnd());
            assertEquals("MAX", response.getPlan());
        }

        @Test
        void cancelAtPeriodEnd为null返回false() {
            StripeSubscription sub = new StripeSubscription();
            sub.setId(1L);
            sub.setUserId(1L);
            sub.setPlan("PRO");
            sub.setStatus("active");
            sub.setCurrentPeriodEnd(LocalDateTime.of(2026, 5, 1, 0, 0));
            sub.setCancelAtPeriodEnd(null);

            when(billingPort.findSubscriptionsByUserId(1L)).thenReturn(List.of(sub));

            SubscriptionStatusResponse response = subscriptionService.getSubscriptionStatus(1L);

            assertFalse(response.getCancelAtPeriodEnd());
        }
    }

    // ==================== createCheckoutSession 错误路径 ====================

    @Nested
    @DisplayName("createCheckoutSession 参数校验")
    class CreateCheckoutSessionValidationTests {

        @Test
        void 无效套餐类型抛出异常() {
            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("INVALID");
            request.setBillingCycle("monthly");

            IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
                    () -> subscriptionService.createCheckoutSession(1L, request));

            assertTrue(ex.getMessage().contains("无效的套餐类型"));
        }

        @Test
        void 无效计费周期抛出异常() {
            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("PRO");
            request.setBillingCycle("INVALID");

            IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
                    () -> subscriptionService.createCheckoutSession(1L, request));

            assertTrue(ex.getMessage().contains("无效的计费周期"));
        }
    }

    // ==================== createCheckoutSession 成功路径 ====================

    @Nested
    @DisplayName("createCheckoutSession 成功路径")
    class CreateCheckoutSessionSuccessTests {

        @Test
        void 创建新客户并生成CheckoutSession() {
            when(billingPort.findCustomerByUserIdAndNotDeleted(1L))
                    .thenReturn(null);

            User user = new User();
            user.setId(1L);
            user.setEmail("test@example.com");
            when(userRepositoryPort.findById(1L)).thenReturn(Optional.of(user));
            when(paymentPort.createCustomer(anyString())).thenReturn(new com.yumu.noveltranslator.port.out.payment.CustomerInfo("cus_test123", "test@example.com"));

            StripeProperties.PlanPrices planPrices = new StripeProperties.PlanPrices();
            planPrices.setMonthlyPriceId("price_pro_monthly");
            when(stripeProperties.getPrices()).thenReturn(Map.of("pro", planPrices));
            when(stripeProperties.getSuccessUrl()).thenReturn("https://example.com/success");
            when(stripeProperties.getCancelUrl()).thenReturn("https://example.com/cancel");

            when(paymentPort.createCheckoutSession(eq("cus_test123"), eq("price_pro_monthly"), eq("https://example.com/success"), eq("https://example.com/cancel"), anyMap()))
                    .thenReturn("https://checkout.stripe.com/test");

            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("PRO");
            request.setBillingCycle("monthly");

            CheckoutSessionResponse response = subscriptionService.createCheckoutSession(1L, request);

            assertNotNull(response);
            assertEquals("https://checkout.stripe.com/test", response.getCheckoutUrl());
            verify(billingPort).saveCustomer(any(StripeCustomer.class));
        }

        @Test
        void 使用已有客户生成CheckoutSession() {
            StripeCustomer existing = new StripeCustomer();
            existing.setId(1L);
            existing.setUserId(1L);
            existing.setStripeCustomerId("cus_existing");
            when(billingPort.findCustomerByUserIdAndNotDeleted(1L))
                    .thenReturn(existing);

            StripeProperties.PlanPrices planPrices = new StripeProperties.PlanPrices();
            planPrices.setYearlyPriceId("price_pro_yearly");
            when(stripeProperties.getPrices()).thenReturn(Map.of("pro", planPrices));
            when(stripeProperties.getSuccessUrl()).thenReturn("https://example.com/success");
            when(stripeProperties.getCancelUrl()).thenReturn("https://example.com/cancel");

            when(paymentPort.createCheckoutSession(eq("cus_existing"), eq("price_pro_yearly"), eq("https://example.com/success"), eq("https://example.com/cancel"), anyMap()))
                    .thenReturn("https://checkout.stripe.com/test2");

            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("PRO");
            request.setBillingCycle("yearly");

            CheckoutSessionResponse response = subscriptionService.createCheckoutSession(1L, request);

            assertNotNull(response);
            assertEquals("https://checkout.stripe.com/test2", response.getCheckoutUrl());
            verify(billingPort, never()).saveCustomer(any());
        }

        @Test
        void 用户不存在时抛出异常() {
            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("PRO");
            request.setBillingCycle("monthly");

            RuntimeException ex = assertThrows(RuntimeException.class,
                    () -> subscriptionService.createCheckoutSession(1L, request));

            assertTrue(ex.getMessage().contains("Stripe prices") || ex.getMessage().contains("No price configured"));
        }
    }

    // ==================== cancelSubscription ====================

    @Nested
    @DisplayName("cancelSubscription")
    class CancelSubscriptionTests {

        @Test
        void 没有活跃订阅抛出异常() {
            when(billingPort.findActiveSubscriptionByUserId(1L)).thenReturn(null);

            RuntimeException ex = assertThrows(RuntimeException.class,
                    () -> subscriptionService.cancelSubscription(1L));

            assertTrue(ex.getMessage().contains("没有可取消的活跃订阅"));
        }

        @Test
        void 成功取消订阅() {
            StripeSubscription sub = new StripeSubscription();
            sub.setId(1L);
            sub.setUserId(1L);
            sub.setPlan("PRO");
            sub.setStatus("active");
            sub.setStripeSubscriptionId("sub_test123");
            sub.setCurrentPeriodEnd(LocalDateTime.of(2026, 6, 1, 0, 0));

            when(billingPort.findActiveSubscriptionByUserId(1L)).thenReturn(sub);
            when(paymentPort.updateSubscription(eq("sub_test123"), any()))
                    .thenReturn(new com.yumu.noveltranslator.port.out.payment.SubscriptionInfo("sub_test123", "active", null, null, true, null, null, null));

            SubscriptionStatusResponse response = subscriptionService.cancelSubscription(1L);

            assertNotNull(response);
            assertTrue(response.getCancelAtPeriodEnd());
            verify(paymentPort).updateSubscription(eq("sub_test123"), any());
            verify(billingPort).updateSubscription(argThat(s ->
                    Boolean.TRUE.equals(s.getCancelAtPeriodEnd()) && s.getCanceledAt() != null));
        }
    }

    // ==================== createPortalSession ====================

    @Nested
    @DisplayName("createPortalSession")
    class CreatePortalSessionTests {

        @Test
        void 客户不存在抛出异常() {
            when(billingPort.findCustomerByUserIdAndNotDeleted(1L)).thenReturn(null);

            RuntimeException ex = assertThrows(RuntimeException.class,
                    () -> subscriptionService.createPortalSession(1L));

            assertTrue(ex.getMessage().contains("未找到 Stripe 客户"));
        }

        @Test
        void 成功创建PortalSession() {
            StripeCustomer customer = new StripeCustomer();
            customer.setId(1L);
            customer.setUserId(1L);
            customer.setStripeCustomerId("cus_test123");
            when(billingPort.findCustomerByUserIdAndNotDeleted(1L)).thenReturn(customer);
            when(stripeProperties.getCancelUrl()).thenReturn("https://example.com/return");
            when(paymentPort.createBillingPortalSession("cus_test123", "https://example.com/return"))
                    .thenReturn("https://billing.stripe.com/test");

            PortalSessionResponse response = subscriptionService.createPortalSession(1L);

            assertNotNull(response);
            assertEquals("https://billing.stripe.com/test", response.getPortalUrl());
        }
    }

    // ==================== Webhook: handleCheckoutSessionCompleted ====================

    @Nested
    @DisplayName("Webhook: handleCheckoutSessionCompleted")
    class HandleCheckoutSessionCompletedTests {

        @Test
        void 反序列化失败不抛异常() {
            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.empty());

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleCheckoutSessionCompleted(event));
        }

        @Test
        void 缺少metadata时正常返回() {
            Session session = mock(Session.class);
            when(session.getId()).thenReturn("cs_test123");
            when(session.getMetadata()).thenReturn(Map.of());

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(session));

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleCheckoutSessionCompleted(event));
        }

        @Test
        void session中没有subscription时正常返回() {
            Session session = mock(Session.class);
            when(session.getId()).thenReturn("cs_test123");
            when(session.getMetadata()).thenReturn(Map.of(
                    "userId", "1",
                    "plan", "PRO",
                    "billingCycle", "monthly"
            ));
            when(session.getSubscription()).thenReturn(null);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(session));

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleCheckoutSessionCompleted(event));
        }

        @Test
        void StripeAPI调用失败正常返回() {
            Session session = mock(Session.class);
            when(session.getId()).thenReturn("cs_test123");
            when(session.getMetadata()).thenReturn(Map.of(
                    "userId", "1",
                    "plan", "PRO",
                    "billingCycle", "monthly"
            ));
            when(session.getSubscription()).thenReturn("sub_test123");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(session));

            when(paymentPort.retrieveSubscription("sub_test123"))
                    .thenThrow(new RuntimeException("Stripe API error"));

            assertThrows(RuntimeException.class, () -> subscriptionService.handleCheckoutSessionCompleted(event));
        }
    }

    // ==================== Webhook: handleSubscriptionUpdated ====================

    @Nested
    @DisplayName("Webhook: handleSubscriptionUpdated")
    class HandleSubscriptionUpdatedTests {

        @Test
        void 反序列化失败不抛异常() {
            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.empty());

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleSubscriptionUpdated(event));
        }

        @Test
        void 本地无记录时正常返回() {
            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_nonexistent");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));

            when(billingPort.findSubscriptionByStripeId("sub_nonexistent")).thenReturn(null);

            assertDoesNotThrow(() -> subscriptionService.handleSubscriptionUpdated(event));
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 幂等检查跳过重复事件() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_test123");

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");

            com.stripe.model.SubscriptionItem subItem = mock(com.stripe.model.SubscriptionItem.class);
            com.stripe.model.Price price = mock(com.stripe.model.Price.class);
            when(price.getId()).thenReturn("price_test");
            when(subItem.getPrice()).thenReturn(price);
            SubscriptionItemCollection items = mock(SubscriptionItemCollection.class);
            when(items.getData()).thenReturn(List.of(subItem));
            when(stripeSub.getItems()).thenReturn(items);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_test123");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);

            assertDoesNotThrow(() -> subscriptionService.handleSubscriptionUpdated(event));

            // 原子更新会执行，但幂等检查会阻止实际更新
            verify(billingPort).atomicUpdateSubscription(anyLong(), anyString(), anyString(), anyLong());
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 状态变为canceled时降级为FREE() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_old");

            User user = new User();
            user.setId(1L);
            user.setUserLevel("PRO");
            when(userRepositoryPort.findById(1L)).thenReturn(Optional.of(user));
            doNothing().when(userRepositoryPort).update(any(User.class));
            doNothing().when(userRepositoryPort).savePlanHistory(any(UserPlanHistory.class));

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");
            when(stripeSub.getStatus()).thenReturn("canceled");
            when(stripeSub.getCancelAtPeriodEnd()).thenReturn(false);
            when(stripeSub.getCurrentPeriodStart()).thenReturn(null);
            when(stripeSub.getCurrentPeriodEnd()).thenReturn(null);
            when(stripeSub.getCanceledAt()).thenReturn(null);

            com.stripe.model.SubscriptionItem subItem = mock(com.stripe.model.SubscriptionItem.class);
            com.stripe.model.Price price = mock(com.stripe.model.Price.class);
            when(price.getId()).thenReturn("price_test");
            when(subItem.getPrice()).thenReturn(price);
            SubscriptionItemCollection items = mock(SubscriptionItemCollection.class);
            when(items.getData()).thenReturn(List.of(subItem));
            when(stripeSub.getItems()).thenReturn(items);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_new123");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);
            when(billingPort.atomicUpdateSubscription(anyLong(), anyString(), anyString(), anyLong())).thenReturn(1);
            ValueOperations<String, String> valueOps = mock(ValueOperations.class);
            when(stringRedisTemplate.opsForValue()).thenReturn(valueOps);
            when(valueOps.setIfAbsent(anyString(), anyString(), any())).thenReturn(true);

            subscriptionService.handleSubscriptionUpdated(event);

            verify(userRepositoryPort).update(argThat(u -> "FREE".equals(u.getUserLevel())));
            verify(userRepositoryPort).savePlanHistory(argThat(h ->
                    "PRO".equals(h.getOldPlan()) && "FREE".equals(h.getNewPlan())));
        }
    }

    // ==================== Webhook: handleSubscriptionDeleted ====================

    @Nested
    @DisplayName("Webhook: handleSubscriptionDeleted")
    class HandleSubscriptionDeletedTests {

        @Test
        void 反序列化失败不抛异常() {
            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.empty());

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleSubscriptionDeleted(event));
        }

        @Test
        void 本地无记录时正常返回() {
            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_nonexistent");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));

            when(billingPort.findSubscriptionByStripeId("sub_nonexistent")).thenReturn(null);

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleSubscriptionDeleted(event));
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 幂等检查跳过重复事件() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_test123");

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_test123");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);

            assertDoesNotThrow(() -> subscriptionService.handleSubscriptionDeleted(event));

            verify(billingPort, never()).updateSubscription(any());
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 删除订阅并降级用户为FREE() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_old");

            User user = new User();
            user.setId(1L);
            user.setUserLevel("PRO");
            when(userRepositoryPort.findById(1L)).thenReturn(Optional.of(user));
            doNothing().when(userRepositoryPort).update(any(User.class));
            doNothing().when(userRepositoryPort).savePlanHistory(any(UserPlanHistory.class));

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_deleted123");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);
            when(billingPort.atomicUpdateSubscription(anyLong(), anyString(), anyString(), anyLong())).thenReturn(1);
            ValueOperations<String, String> valueOps = mock(ValueOperations.class);
            when(stringRedisTemplate.opsForValue()).thenReturn(valueOps);
            when(valueOps.setIfAbsent(anyString(), anyString(), any())).thenReturn(true);

            subscriptionService.handleSubscriptionDeleted(event);

            verify(billingPort).atomicUpdateSubscription(anyLong(), anyString(), anyString(), anyLong());
            verify(userRepositoryPort).update(argThat(u -> "FREE".equals(u.getUserLevel())));
            verify(userRepositoryPort).savePlanHistory(argThat(h ->
                    "subscription.deleted".equals(h.getNote())));
        }
    }

    // ==================== Webhook: handleSubscriptionResumed ====================

    @Nested
    @DisplayName("Webhook: handleSubscriptionResumed")
    class HandleSubscriptionResumedTests {

        @Test
        void 反序列化失败不抛异常() {
            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.empty());

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleSubscriptionResumed(event));
        }

        @Test
        void 本地无记录时正常返回() {
            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_nonexistent");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));

            when(billingPort.findSubscriptionByStripeId("sub_nonexistent")).thenReturn(null);

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleSubscriptionResumed(event));
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 更新订阅状态() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("paused");

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");
            when(stripeSub.getStatus()).thenReturn("active");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_resumed123");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);

            subscriptionService.handleSubscriptionResumed(event);

            verify(billingPort).updateSubscription(argThat(s ->
                    "active".equals(s.getStatus()) && "evt_resumed123".equals(s.getLastWebhookEventId())));
        }
    }

    // ==================== Webhook: handleInvoicePaymentFailed ====================

    @Nested
    @DisplayName("Webhook: handleInvoicePaymentFailed")
    class HandleInvoicePaymentFailedTests {

        @Test
        void 反序列化失败不抛异常() {
            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.empty());

            assertThrows(IllegalStateException.class, () -> subscriptionService.handleInvoicePaymentFailed(event));
        }

        @Test
        void 发票无订阅信息时正常返回() {
            com.stripe.model.Invoice invoice = mock(com.stripe.model.Invoice.class);
            when(invoice.getSubscription()).thenReturn(null);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(invoice));

            assertDoesNotThrow(() -> subscriptionService.handleInvoicePaymentFailed(event));
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 有本地记录时标记为past_due() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);

            com.stripe.model.Invoice invoice = mock(com.stripe.model.Invoice.class);
            when(invoice.getSubscription()).thenReturn("sub_test123");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(invoice));
            when(event.getId()).thenReturn("evt_payment_failed123");

            subscriptionService.handleInvoicePaymentFailed(event);

            verify(billingPort).updateSubscription(argThat(s ->
                    "past_due".equals(s.getStatus()) && "evt_payment_failed123".equals(s.getLastWebhookEventId())));
        }

        @Test
        void 无本地记录时仅记录警告() {
            when(billingPort.findSubscriptionByStripeId("sub_unknown")).thenReturn(null);

            com.stripe.model.Invoice invoice = mock(com.stripe.model.Invoice.class);
            when(invoice.getSubscription()).thenReturn("sub_unknown");

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(invoice));

            assertDoesNotThrow(() -> subscriptionService.handleInvoicePaymentFailed(event));

            verify(billingPort, never()).updateSubscription(any());
        }
    }

    // ==================== getOrCreateCustomer 间接测试 ====================

    @Nested
    @DisplayName("getOrCreateCustomer 间接测试")
    class GetOrCreateCustomerIndirectTests {

        @Test
        void 返回已有客户() {
            StripeCustomer existing = new StripeCustomer();
            existing.setId(1L);
            existing.setUserId(1L);
            existing.setStripeCustomerId("cus_existing");
            when(billingPort.findCustomerByUserIdAndNotDeleted(1L)).thenReturn(existing);

            StripeProperties.PlanPrices planPrices = new StripeProperties.PlanPrices();
            planPrices.setMonthlyPriceId("price_pro_monthly");
            when(stripeProperties.getPrices()).thenReturn(Map.of("pro", planPrices));
            when(stripeProperties.getSuccessUrl()).thenReturn("https://example.com/success");
            when(stripeProperties.getCancelUrl()).thenReturn("https://example.com/cancel");

            when(paymentPort.createCheckoutSession(eq("cus_existing"), eq("price_pro_monthly"), eq("https://example.com/success"), eq("https://example.com/cancel"), anyMap()))
                    .thenReturn("https://checkout.stripe.com/test");

            CheckoutSessionRequest request = new CheckoutSessionRequest();
            request.setPlan("PRO");
            request.setBillingCycle("monthly");

            subscriptionService.createCheckoutSession(1L, request);

            verify(billingPort, never()).saveCustomer(any());
            verify(userRepositoryPort, never()).findById(anyLong());
        }
    }

    // ==================== updateUserLevel 间接测试 ====================

    @Nested
    @DisplayName("updateUserLevel 间接测试")
    class UpdateUserLevelIndirectTests {

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 用户不存在时不抛异常() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(999L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_old");

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");
            when(stripeSub.getStatus()).thenReturn("canceled");
            when(stripeSub.getCancelAtPeriodEnd()).thenReturn(false);
            when(stripeSub.getCurrentPeriodStart()).thenReturn(null);
            when(stripeSub.getCurrentPeriodEnd()).thenReturn(null);
            when(stripeSub.getCanceledAt()).thenReturn(null);

            com.stripe.model.SubscriptionItem subItem = mock(com.stripe.model.SubscriptionItem.class);
            com.stripe.model.Price price = mock(com.stripe.model.Price.class);
            when(price.getId()).thenReturn("price_test");
            when(subItem.getPrice()).thenReturn(price);
            SubscriptionItemCollection items = mock(SubscriptionItemCollection.class);
            when(items.getData()).thenReturn(List.of(subItem));
            when(stripeSub.getItems()).thenReturn(items);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_new456");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);
            when(userRepositoryPort.findById(999L)).thenReturn(Optional.empty());

            assertDoesNotThrow(() -> subscriptionService.handleSubscriptionUpdated(event));
        }

        @Disabled("LambdaUpdateWrapper 需要 Spring 上下文初始化实体缓存")
        @Test
        void 用户等级相同时不更新() {
            StripeSubscription subRecord = new StripeSubscription();
            subRecord.setId(1L);
            subRecord.setUserId(1L);
            subRecord.setStripeSubscriptionId("sub_test123");
            subRecord.setStatus("active");
            subRecord.setPlan("PRO");
            subRecord.setLastWebhookEventId("evt_old");

            User user = new User();
            user.setId(1L);
            user.setUserLevel("PRO");
            when(userRepositoryPort.findById(1L)).thenReturn(Optional.of(user));

            Subscription stripeSub = mock(Subscription.class);
            when(stripeSub.getId()).thenReturn("sub_test123");
            when(stripeSub.getStatus()).thenReturn("active");
            when(stripeSub.getCancelAtPeriodEnd()).thenReturn(false);
            when(stripeSub.getCurrentPeriodStart()).thenReturn(null);
            when(stripeSub.getCurrentPeriodEnd()).thenReturn(null);
            when(stripeSub.getCanceledAt()).thenReturn(null);

            com.stripe.model.SubscriptionItem subItem = mock(com.stripe.model.SubscriptionItem.class);
            com.stripe.model.Price price = mock(com.stripe.model.Price.class);
            when(price.getId()).thenReturn("price_test");
            when(subItem.getPrice()).thenReturn(price);
            SubscriptionItemCollection items = mock(SubscriptionItemCollection.class);
            when(items.getData()).thenReturn(List.of(subItem));
            when(stripeSub.getItems()).thenReturn(items);

            Event event = mock(Event.class);
            EventDataObjectDeserializer deserializer = mock(EventDataObjectDeserializer.class);
            when(event.getDataObjectDeserializer()).thenReturn(deserializer);
            when(deserializer.getObject()).thenReturn(Optional.of(stripeSub));
            when(event.getId()).thenReturn("evt_new789");

            when(billingPort.findSubscriptionByStripeId("sub_test123")).thenReturn(subRecord);

            subscriptionService.handleSubscriptionUpdated(event);

            verify(userRepositoryPort, never()).update(any());
            verify(userRepositoryPort, never()).savePlanHistory(any());
        }
    }
}
