package com.flightticket.config;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnStateTransitionEvent;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

/**
 * Unit tests for ResilienceConfig circuit breaker event listeners
 * 
 * Tests verify that circuit breaker state changes are properly logged
 * with timestamp, circuit breaker name, and reason.
 * 
 * Requirements: 9.5
 */
@ExtendWith(MockitoExtension.class)
class ResilienceConfigTest {
    
    @Mock
    private CircuitBreakerRegistry circuitBreakerRegistry;
    
    @Mock
    private RetryRegistry retryRegistry;
    
    @Mock
    private TimeLimiterRegistry timeLimiterRegistry;
    
    private ResilienceConfig resilienceConfig;
    
    @BeforeEach
    void setUp() {
        resilienceConfig = new ResilienceConfig(
            circuitBreakerRegistry,
            retryRegistry,
            timeLimiterRegistry
        );
    }
    
    /**
     * Test that circuit breaker event listeners are configured for Expedia
     * and that state transition events are captured
     */
    @Test
    void shouldConfigureExpediaCircuitBreakerEventListeners() {
        // Arrange
        CircuitBreaker mockAmadeusCircuitBreaker = mock(CircuitBreaker.class);
        CircuitBreaker.EventPublisher mockAmadeusEventPublisher = mock(CircuitBreaker.EventPublisher.class);
        
        CircuitBreaker mockExpediaCircuitBreaker = mock(CircuitBreaker.class);
        CircuitBreaker.EventPublisher mockExpediaEventPublisher = mock(CircuitBreaker.EventPublisher.class);
        
        // Mock Amadeus circuit breaker
        when(circuitBreakerRegistry.circuitBreaker("amadeus")).thenReturn(mockAmadeusCircuitBreaker);
        when(mockAmadeusCircuitBreaker.getEventPublisher()).thenReturn(mockAmadeusEventPublisher);
        when(mockAmadeusEventPublisher.onStateTransition(any())).thenReturn(mockAmadeusEventPublisher);
        when(mockAmadeusEventPublisher.onError(any())).thenReturn(mockAmadeusEventPublisher);
        when(mockAmadeusEventPublisher.onSuccess(any())).thenReturn(mockAmadeusEventPublisher);
        
        // Mock Expedia circuit breaker
        when(circuitBreakerRegistry.circuitBreaker("expedia")).thenReturn(mockExpediaCircuitBreaker);
        when(mockExpediaCircuitBreaker.getEventPublisher()).thenReturn(mockExpediaEventPublisher);
        when(mockExpediaEventPublisher.onStateTransition(any())).thenReturn(mockExpediaEventPublisher);
        when(mockExpediaEventPublisher.onError(any())).thenReturn(mockExpediaEventPublisher);
        when(mockExpediaEventPublisher.onSuccess(any())).thenReturn(mockExpediaEventPublisher);
        
        // Act
        resilienceConfig.configureCircuitBreakerEventListeners();
        
        // Assert - Verify Expedia circuit breaker was configured
        verify(circuitBreakerRegistry).circuitBreaker("expedia");
        verify(mockExpediaCircuitBreaker).getEventPublisher();
        verify(mockExpediaEventPublisher).onStateTransition(any());
        verify(mockExpediaEventPublisher).onError(any());
        verify(mockExpediaEventPublisher).onSuccess(any());
    }
    
    /**
     * Test that state transition event listener is invoked with correct parameters
     */
    @Test
    void shouldLogStateTransitionWithTimestampAndReason() {
        // Arrange
        CircuitBreaker realCircuitBreaker = CircuitBreaker.of("expedia", 
            CircuitBreakerConfig.custom()
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(1))
                .build()
        );
        
        AtomicInteger stateTransitionCount = new AtomicInteger(0);
        
        // Register event listener
        realCircuitBreaker.getEventPublisher()
            .onStateTransition(event -> {
                stateTransitionCount.incrementAndGet();
                
                // Verify event contains required information
                assertThat(event.getCircuitBreakerName()).isEqualTo("expedia");
                assertThat(event.getCreationTime()).isNotNull();
                assertThat(event.getStateTransition()).isNotNull();
                assertThat(event.getStateTransition().getFromState()).isNotNull();
                assertThat(event.getStateTransition().getToState()).isNotNull();
            });
        
        // Act - Force state transition from CLOSED to OPEN
        realCircuitBreaker.transitionToOpenState();
        
        // Assert
        assertThat(stateTransitionCount.get()).isGreaterThan(0);
        assertThat(realCircuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
    }
    
    /**
     * Test that circuit breaker transitions through all states
     * and events are captured for each transition
     */
    @Test
    void shouldCaptureAllStateTransitions() {
        // Arrange
        CircuitBreaker realCircuitBreaker = CircuitBreaker.of("expedia", 
            CircuitBreakerConfig.custom()
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofMillis(100))
                .permittedNumberOfCallsInHalfOpenState(2)
                .build()
        );
        
        AtomicInteger closedToOpenCount = new AtomicInteger(0);
        AtomicInteger openToHalfOpenCount = new AtomicInteger(0);
        AtomicInteger halfOpenToClosedCount = new AtomicInteger(0);
        
        // Register event listener
        realCircuitBreaker.getEventPublisher()
            .onStateTransition(event -> {
                CircuitBreaker.State fromState = event.getStateTransition().getFromState();
                CircuitBreaker.State toState = event.getStateTransition().getToState();
                
                if (fromState == CircuitBreaker.State.CLOSED && toState == CircuitBreaker.State.OPEN) {
                    closedToOpenCount.incrementAndGet();
                }
                if (fromState == CircuitBreaker.State.OPEN && toState == CircuitBreaker.State.HALF_OPEN) {
                    openToHalfOpenCount.incrementAndGet();
                }
                if (fromState == CircuitBreaker.State.HALF_OPEN && toState == CircuitBreaker.State.CLOSED) {
                    halfOpenToClosedCount.incrementAndGet();
                }
            });
        
        // Act - Transition CLOSED -> OPEN
        realCircuitBreaker.transitionToOpenState();
        assertThat(closedToOpenCount.get()).isEqualTo(1);
        
        // Transition OPEN -> HALF_OPEN
        realCircuitBreaker.transitionToHalfOpenState();
        assertThat(openToHalfOpenCount.get()).isEqualTo(1);
        
        // Transition HALF_OPEN -> CLOSED
        realCircuitBreaker.transitionToClosedState();
        assertThat(halfOpenToClosedCount.get()).isEqualTo(1);
    }
    
    /**
     * Test that error events are captured with timestamp
     */
    @Test
    void shouldCaptureErrorEventsWithTimestamp() {
        // Arrange
        CircuitBreaker realCircuitBreaker = CircuitBreaker.of("expedia", 
            CircuitBreakerConfig.custom()
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .build()
        );
        
        AtomicInteger errorCount = new AtomicInteger(0);
        
        // Register error event listener
        realCircuitBreaker.getEventPublisher()
            .onError(event -> {
                errorCount.incrementAndGet();
                
                // Verify event contains required information
                assertThat(event.getCircuitBreakerName()).isEqualTo("expedia");
                assertThat(event.getCreationTime()).isNotNull();
                assertThat(event.getThrowable()).isNotNull();
            });
        
        // Act - Record an error
        try {
            realCircuitBreaker.executeSupplier(() -> {
                throw new RuntimeException("Test error");
            });
        } catch (Exception e) {
            // Expected
        }
        
        // Assert
        assertThat(errorCount.get()).isEqualTo(1);
    }
    
    /**
     * Test that success events are captured with timestamp
     */
    @Test
    void shouldCaptureSuccessEventsWithTimestamp() {
        // Arrange
        CircuitBreaker realCircuitBreaker = CircuitBreaker.of("expedia", 
            CircuitBreakerConfig.custom()
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .build()
        );
        
        AtomicInteger successCount = new AtomicInteger(0);
        
        // Register success event listener
        realCircuitBreaker.getEventPublisher()
            .onSuccess(event -> {
                successCount.incrementAndGet();
                
                // Verify event contains required information
                assertThat(event.getCircuitBreakerName()).isEqualTo("expedia");
                assertThat(event.getCreationTime()).isNotNull();
            });
        
        // Act - Record a success
        String result = realCircuitBreaker.executeSupplier(() -> "success");
        
        // Assert
        assertThat(result).isEqualTo("success");
        assertThat(successCount.get()).isEqualTo(1);
    }
}
