package com.saas.subscription.service.impl;

import com.saas.shared.dto.mapper.SubscriptionPlanMapper;
import com.saas.shared.exception.BusinessException;
import com.saas.shared.exception.ErrorCode;
import com.saas.subscription.dto.request.CreateSubscriptionPlanRequest;
import com.saas.subscription.dto.response.SubscriptionPlanResponse;
import com.saas.subscription.entity.SubscriptionPlan;
import com.saas.subscription.repository.SubscriptionPlanRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

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

/**
 * Unit Tests for SubscriptionPlanServiceImpl
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("SubscriptionPlanService Unit Tests")
class SubscriptionPlanServiceImplTest {

    @Mock
    private SubscriptionPlanRepository planRepository;

    @Mock
    private SubscriptionPlanMapper planMapper;

    @InjectMocks
    private SubscriptionPlanServiceImpl planService;

    private CreateSubscriptionPlanRequest testRequest;
    private SubscriptionPlan testPlan;
    private SubscriptionPlanResponse testResponse;

    @BeforeEach
    void setUp() {
        testRequest = CreateSubscriptionPlanRequest.builder()
            .name("PRO")
            .description("Professional plan for growing businesses")
            .monthlyPrice(new BigDecimal("99.00"))
            .annualPrice(new BigDecimal("990.00"))
            .maxConcurrentCalls(10)
            .maxAiMinutes(5000)
            .maxAiAgents(5)
            .maxUsers(50)
            .maxPhoneNumbers(10)
            .storageQuotaGb(100)
            .build();

        testPlan = SubscriptionPlan.builder()
            .id(1L)
            .name("PRO")
            .description("Professional plan for growing businesses")
            .monthlyPrice(new BigDecimal("99.00"))
            .annualPrice(new BigDecimal("990.00"))
            .maxConcurrentCalls(10)
            .maxAiMinutes(5000)
            .maxAiAgents(5)
            .maxUsers(50)
            .maxPhoneNumbers(10)
            .storageQuotaGb(100)
            .isActive(true)
            .build();

        testResponse = SubscriptionPlanResponse.builder()
            .id(1L)
            .name("PRO")
            .description("Professional plan for growing businesses")
            .monthlyPrice(new BigDecimal("99.00"))
            .isActive(true)
            .build();
    }

    @Test
    @DisplayName("Should create subscription plan successfully")
    void testCreatePlan() {
        // Arrange
        when(planRepository.findByName(testRequest.getName())).thenReturn(Optional.empty());
        when(planRepository.save(any(SubscriptionPlan.class))).thenReturn(testPlan);
        when(planMapper.toResponse(testPlan)).thenReturn(testResponse);

        // Act
        SubscriptionPlanResponse result = planService.createPlan(testRequest);

        // Assert
        assertNotNull(result);
        assertEquals(testResponse.getName(), result.getName());
        verify(planRepository, times(1)).findByName(testRequest.getName());
        verify(planRepository, times(1)).save(any(SubscriptionPlan.class));
        verify(planMapper, times(1)).toResponse(testPlan);
    }

    @Test
    @DisplayName("Should throw exception when plan name already exists")
    void testCreatePlanDuplicateName() {
        // Arrange
        when(planRepository.findByName(testRequest.getName())).thenReturn(Optional.of(testPlan));

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> planService.createPlan(testRequest));
        
        assertEquals(ErrorCode.SUBSCRIPTION_PLAN_ALREADY_EXISTS, exception.getErrorCode());
        verify(planRepository, times(1)).findByName(testRequest.getName());
        verify(planRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should throw exception when annual price exceeds 12x monthly")
    void testCreatePlanInvalidAnnualPrice() {
        // Arrange
        testRequest.setAnnualPrice(new BigDecimal("1500.00")); // More than 12 * 99
        when(planRepository.findByName(testRequest.getName())).thenReturn(Optional.empty());

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> planService.createPlan(testRequest));
        
        assertEquals(ErrorCode.INVALID_INPUT, exception.getErrorCode());
        verify(planRepository, never()).save(any());
    }

    @Test
    @DisplayName("Should get plan by ID successfully")
    void testGetPlan() {
        // Arrange
        when(planRepository.findById(1L)).thenReturn(Optional.of(testPlan));
        when(planMapper.toResponse(testPlan)).thenReturn(testResponse);

        // Act
        SubscriptionPlanResponse result = planService.getPlan(1L);

        // Assert
        assertNotNull(result);
        assertEquals(testResponse.getName(), result.getName());
        verify(planRepository, times(1)).findById(1L);
        verify(planMapper, times(1)).toResponse(testPlan);
    }

    @Test
    @DisplayName("Should throw exception when plan not found")
    void testGetPlanNotFound() {
        // Arrange
        when(planRepository.findById(999L)).thenReturn(Optional.empty());

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> planService.getPlan(999L));
        
        assertEquals(ErrorCode.SUBSCRIPTION_PLAN_NOT_FOUND, exception.getErrorCode());
    }

    @Test
    @DisplayName("Should get all active plans")
    void testGetAllActivePlans() {
        // Arrange
        List<SubscriptionPlan> plans = List.of(testPlan);
        when(planRepository.findAllActive()).thenReturn(plans);
        when(planMapper.toResponse(testPlan)).thenReturn(testResponse);

        // Act
        List<SubscriptionPlanResponse> result = planService.getAllActivePlans();

        // Assert
        assertNotNull(result);
        assertEquals(1, result.size());
        assertEquals(testResponse.getName(), result.get(0).getName());
        verify(planRepository, times(1)).findAllActive();
    }

    @Test
    @DisplayName("Should update plan successfully")
    void testUpdatePlan() {
        // Arrange
        when(planRepository.findById(1L)).thenReturn(Optional.of(testPlan));
        when(planRepository.existsNameExcludingId(testRequest.getName(), 1L)).thenReturn(false);
        when(planRepository.save(testPlan)).thenReturn(testPlan);
        when(planMapper.toResponse(testPlan)).thenReturn(testResponse);

        // Act
        SubscriptionPlanResponse result = planService.updatePlan(1L, testRequest);

        // Assert
        assertNotNull(result);
        verify(planRepository, times(1)).findById(1L);
        verify(planRepository, times(1)).save(testPlan);
    }

    @Test
    @DisplayName("Should throw exception when updating with duplicate name")
    void testUpdatePlanDuplicateName() {
        // Arrange
        testRequest.setName("NEW_NAME");
        when(planRepository.findById(1L)).thenReturn(Optional.of(testPlan));
        when(planRepository.existsNameExcludingId("NEW_NAME", 1L)).thenReturn(true);

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> planService.updatePlan(1L, testRequest));
        
        assertEquals(ErrorCode.SUBSCRIPTION_PLAN_ALREADY_EXISTS, exception.getErrorCode());
    }

    @Test
    @DisplayName("Should delete plan successfully")
    void testDeletePlan() {
        // Arrange
        when(planRepository.existsById(1L)).thenReturn(true);
        doNothing().when(planRepository).deactivate(1L);

        // Act
        planService.deletePlan(1L);

        // Assert
        verify(planRepository, times(1)).existsById(1L);
        verify(planRepository, times(1)).deactivate(1L);
    }

    @Test
    @DisplayName("Should throw exception when deleting non-existent plan")
    void testDeletePlanNotFound() {
        // Arrange
        when(planRepository.existsById(999L)).thenReturn(false);

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class,
            () -> planService.deletePlan(999L));
        
        assertEquals(ErrorCode.SUBSCRIPTION_PLAN_NOT_FOUND, exception.getErrorCode());
        verify(planRepository, never()).deactivate(anyLong());
    }

    @Test
    @DisplayName("Should check plan availability")
    void testIsPlanAvailable() {
        // Arrange
        when(planRepository.existsAndActive(1L)).thenReturn(true);

        // Act
        boolean result = planService.isPlanAvailable(1L);

        // Assert
        assertTrue(result);
        verify(planRepository, times(1)).existsAndActive(1L);
    }
}
