package com.eactive.eai.common.circuitBreaker.type;

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

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;

import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.ApplicationContextProvider;

import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;

/**
 * TypedCircuitBreakerManager 단위 테스트
 *
 * DB / 실 Spring 컨텍스트 없이 동작한다.
 *   - PropManager          : Mockito Mock (DB 없이 설정값 반환)
 *   - TypedCircuitBreakerDAO : Mockito Mock (JPA 없이 VO 반환)
 *   - Spring Bean          : GenericApplicationContext 로 수동 구성
 *
 * 테스트 격리:
 *   각 테스트는 고유한 apiId 를 사용하거나 manager.reload() 로 레지스트리를 초기화한다.
 *   manager.reload() 는 CircuitBreakerRegistry 를 새로 생성하므로 이전 상태가 제거된다.
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
class TypedCircuitBreakerManagerTest {

    // 슬라이딩 윈도우·최소 호출을 4로 설정해 빠른 상태 전이를 유도한다.
    private static final int DEFAULT_FAILURE_RATE      = 50;
    private static final int DEFAULT_SLIDING_WINDOW    = 4;
    private static final int DEFAULT_MIN_CALLS         = 4;
    private static final int DEFAULT_WAIT_DURATION_SEC = 1;   // OPEN 대기 1초 (테스트용)
    private static final int DEFAULT_HALF_OPEN_PERMITS = 2;
    private static final int DEFAULT_SLOW_RATE         = 100;
    private static final int DEFAULT_SLOW_DURATION_SEC = 5;

    private static GenericApplicationContext  ctx;
    private static PropManager                mockPropManager;
    private static TypedCircuitBreakerDAO     mockDao;
    private static TypedCircuitBreakerManager manager;

    // -------------------------------------------------------------------------
    // 픽스처 설정 / 해제
    // -------------------------------------------------------------------------

    @BeforeAll
    static void setUpClass() throws Exception {
        mockPropManager = mock(PropManager.class);
        mockDao         = mock(TypedCircuitBreakerDAO.class);

        setupDefaultPropManagerMock();
        when(mockDao.getList()).thenReturn(Collections.emptyList());

        // TypedCircuitBreakerManager 인스턴스 생성 후 DAO 리플렉션 주입
        manager = new TypedCircuitBreakerManager();
        injectField(manager, "dao", mockDao);

        // DB / 실 Spring 없이 최소 컨텍스트 구성
        ctx = new GenericApplicationContext();
        ctx.getBeanFactory().registerSingleton("propManager", mockPropManager);
        ctx.getBeanFactory().registerSingleton("typedCircuitBreakerManager", manager);
        ctx.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        ctx.refresh();

        manager.start();
    }

    @AfterAll
    static void tearDownClass() {
        if (ctx != null) ctx.close();
    }

    @BeforeEach
    void resetMocks() throws Exception {
        reset(mockDao);
        setupDefaultPropManagerMock();
        when(mockDao.getList()).thenReturn(Collections.emptyList());
    }

    // -------------------------------------------------------------------------
    // 헬퍼
    // -------------------------------------------------------------------------

    private static void setupDefaultPropManagerMock() {
        when(mockPropManager.getProperty("CircuitBreaker", "failureRateThreshold"))
                .thenReturn(String.valueOf(DEFAULT_FAILURE_RATE));
        when(mockPropManager.getProperty("CircuitBreaker", "slidingWindowSize"))
                .thenReturn(String.valueOf(DEFAULT_SLIDING_WINDOW));
        when(mockPropManager.getProperty("CircuitBreaker", "minimumNumberOfCalls"))
                .thenReturn(String.valueOf(DEFAULT_MIN_CALLS));
        when(mockPropManager.getProperty("CircuitBreaker", "waitDurationInOpenState"))
                .thenReturn(String.valueOf(DEFAULT_WAIT_DURATION_SEC));
        when(mockPropManager.getProperty("CircuitBreaker", "permittedNumberOfCallsInHalfOpenState"))
                .thenReturn(String.valueOf(DEFAULT_HALF_OPEN_PERMITS));
        when(mockPropManager.getProperty("CircuitBreaker", "slowCallRateThreshold"))
                .thenReturn(String.valueOf(DEFAULT_SLOW_RATE));
        when(mockPropManager.getProperty("CircuitBreaker", "slowCallDurationThreshold"))
                .thenReturn(String.valueOf(DEFAULT_SLOW_DURATION_SEC));
        when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("1");
    }

    private static void injectField(Object target, String fieldName, Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target, value);
    }

    /** 테스트용 TypedCircuitBreakerVO 생성 (기본 설정값 사용) */
    private TypedCircuitBreakerVO buildVo(String id, String apiId, String useYn) {
        return TypedCircuitBreakerVO.builder()
                .id(id).name(apiId).type("COUNT_BASED").desc(apiId + " 테스트 설정")
                .failureRateThreshold(DEFAULT_FAILURE_RATE)
                .slidingWindowSize(DEFAULT_SLIDING_WINDOW)
                .minimumNumberOfCalls(DEFAULT_MIN_CALLS)
                .waitDurationInOpenState(DEFAULT_WAIT_DURATION_SEC)
                .permittedNumberOfCallsInHalfOpenState(DEFAULT_HALF_OPEN_PERMITS)
                .slowCallRateThreshold(DEFAULT_SLOW_RATE)
                .slowCallDurationThreshold(DEFAULT_SLOW_DURATION_SEC)
                .useYn(useYn)
                .build();
    }

    /** CB를 count 회 실패 처리한다 */
    private void recordFailures(CircuitBreaker cb, int count) {
        for (int i = 0; i < count; i++) {
            try {
                cb.executeCallable(() -> { throw new RuntimeException("테스트 강제 오류"); });
            } catch (Exception ignored) {}
        }
    }

    // =========================================================================
    // 1. getCircuitBreaker - 기본 조회 동작
    // =========================================================================

    @Test
    @DisplayName("1-1. 등록된 apiId → 해당 설정의 CircuitBreaker 반환")
    void testGetCircuitBreaker_registeredApiId_returnsTypedCircuitBreaker() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-001", "API_REG", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_REG");

        assertNotNull(cb);
        assertEquals("API_REG", cb.getName());
    }

    @Test
    @DisplayName("1-2. 미등록 apiId → default CircuitBreaker 로 fallback")
    void testGetCircuitBreaker_unregisteredApiId_returnsDefaultCircuitBreaker() throws Exception {
        when(mockDao.getList()).thenReturn(Collections.emptyList());
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_UNKNOWN");

        assertNotNull(cb, "미등록 apiId 는 default CB 를 반환해야 한다");
        assertEquals("default", cb.getName());
    }

    @Test
    @DisplayName("1-3. useYn=0 인 apiId → default CircuitBreaker 로 fallback")
    void testGetCircuitBreaker_disabledApiId_returnsDefaultCircuitBreaker() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-002", "API_DISABLED", "0")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_DISABLED");

        assertNotNull(cb, "비활성 apiId 는 default CB 로 fallback 되어야 한다");
        assertEquals("default", cb.getName());
    }

    @Test
    @DisplayName("1-4. apiId 도 default 도 비활성(useYn=0) → null 반환")
    void testGetCircuitBreaker_defaultDisabled_returnsNull() throws Exception {
        when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("0");
        when(mockDao.getList()).thenReturn(Collections.emptyList());
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("ANY_API");

        assertNull(cb, "default 도 비활성이면 null 을 반환해야 한다");
    }

    @Test
    @DisplayName("1-5. PropManager useYn=Y → normalizeUseYn 으로 '1' 정규화 → default 활성")
    void testGetCircuitBreaker_defaultUseYn_Y_isNormalizedToEnabled() throws Exception {
        when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("Y");
        when(mockDao.getList()).thenReturn(Collections.emptyList());
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("ANY_API");

        assertNotNull(cb, "PropManager useYn=Y 는 활성으로 정규화되어야 한다");
        assertEquals("default", cb.getName());
    }

    // =========================================================================
    // 2. apiId 별 독립성
    // =========================================================================

    @Test
    @DisplayName("2-1. 서로 다른 apiId → 독립적인 CircuitBreaker 인스턴스 반환")
    void testDifferentApiIds_haveIndependentCircuitBreakerInstances() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(
                buildVo("uuid-A", "API_INDEP_A", "1"),
                buildVo("uuid-B", "API_INDEP_B", "1")));
        manager.reload();

        CircuitBreaker cbA = manager.getCircuitBreaker("API_INDEP_A");
        CircuitBreaker cbB = manager.getCircuitBreaker("API_INDEP_B");

        assertNotNull(cbA);
        assertNotNull(cbB);
        assertNotSame(cbA, cbB, "서로 다른 apiId 는 다른 CB 인스턴스여야 한다");
        assertEquals("API_INDEP_A", cbA.getName());
        assertEquals("API_INDEP_B", cbB.getName());
    }

    @Test
    @DisplayName("2-2. API_A 실패 → OPEN, API_B 는 CLOSED 유지 (상태 격리)")
    void testDifferentApiIds_statesAreIsolated() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(
                buildVo("uuid-ISO-A", "API_ISO_A", "1"),
                buildVo("uuid-ISO-B", "API_ISO_B", "1")));
        manager.reload();

        CircuitBreaker cbA = manager.getCircuitBreaker("API_ISO_A");
        CircuitBreaker cbB = manager.getCircuitBreaker("API_ISO_B");

        // API_ISO_A 만 minimumNumberOfCalls=4 회 실패
        recordFailures(cbA, 4);

        assertEquals(CircuitBreaker.State.OPEN,   cbA.getState(), "API_ISO_A 는 OPEN 이어야 한다");
        assertEquals(CircuitBreaker.State.CLOSED, cbB.getState(), "API_ISO_B 는 CLOSED 를 유지해야 한다");
    }

    // =========================================================================
    // 3. 상태 전이
    // =========================================================================

    @Test
    @DisplayName("3-1. 실패율 50% 초과(4/4 실패) → CLOSED → OPEN")
    void testCircuitBreaker_closedToOpen_afterFailureThresholdExceeded() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-001", "API_ST_OPEN", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_OPEN");
        assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "초기 상태는 CLOSED 여야 한다");

        // minimumNumberOfCalls=4, failureRateThreshold=50% → 4/4 실패(100%) → OPEN
        recordFailures(cb, 4);

        assertEquals(CircuitBreaker.State.OPEN, cb.getState(), "실패율 초과 시 OPEN 상태여야 한다");
    }

    @Test
    @DisplayName("3-2. 실패율 미달(1/4 실패, 25%) → CLOSED 유지")
    void testCircuitBreaker_belowFailureThreshold_remainsClosed() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-002", "API_ST_BELOW", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BELOW");

        // 4회 중 3회 성공, 1회 실패 → 실패율 25% < 50% → CLOSED 유지
        // Resilience4j 는 >= 비교: 50% >= 50% 는 OPEN 이므로 25% 로 낮춰야 CLOSED 유지
        try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
        try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
        try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
        recordFailures(cb, 1);

        assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "실패율 25% (< threshold 50%) 이면 CLOSED 를 유지해야 한다");
    }

    @Test
    @DisplayName("3-3. CB OPEN 상태 → 모든 요청 즉시 차단 (CallNotPermittedException)")
    void testCircuitBreaker_open_blocksAllRequests() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-003", "API_ST_BLOCK", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BLOCK");
        recordFailures(cb, 4);
        assertEquals(CircuitBreaker.State.OPEN, cb.getState());

        assertThrows(CallNotPermittedException.class,
                () -> cb.executeCallable(() -> "도달하면 안 됨"),
                "OPEN 상태에서 CallNotPermittedException 이 발생해야 한다");
    }

    @Test
    @DisplayName("3-4. OPEN → waitDuration(1초) 경과 → 첫 요청 시 HALF_OPEN 전이")
    void testCircuitBreaker_open_transitionsToHalfOpenAfterWaitDuration() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-004", "API_ST_HALFOPEN", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_HALFOPEN");
        recordFailures(cb, 4);
        assertEquals(CircuitBreaker.State.OPEN, cb.getState());

        // waitDurationInOpenState = 1초 대기 후 요청 → HALF_OPEN 전이
        Thread.sleep(1200);
        try { cb.executeCallable(() -> "HALF_OPEN 트리거"); } catch (Exception ignored) {}

        assertEquals(CircuitBreaker.State.HALF_OPEN, cb.getState(),
                "waitDuration 경과 후 첫 요청 시 HALF_OPEN 상태여야 한다");
    }

    @Test
    @DisplayName("3-5. HALF_OPEN → 성공(permittedCalls=2회) → CLOSED 복구")
    void testCircuitBreaker_halfOpen_transitionsToClosedOnSuccess() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-005", "API_ST_RECOVER", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_RECOVER");
        recordFailures(cb, 4);

        Thread.sleep(1200);

        // HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 성공
        for (int i = 0; i < DEFAULT_HALF_OPEN_PERMITS; i++) {
            final String result = cb.executeCallable(() -> "성공");
            assertEquals("성공", result);
        }

        assertEquals(CircuitBreaker.State.CLOSED, cb.getState(),
                "HALF_OPEN 에서 허용 호출 수 이상 성공하면 CLOSED 로 복구되어야 한다");
    }

    @Test
    @DisplayName("3-6. HALF_OPEN → 실패 → 다시 OPEN 재전이")
    void testCircuitBreaker_halfOpen_transitionsBackToOpenOnFailure() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-006", "API_ST_REOPEN", "1")));
        manager.reload();

        CircuitBreaker cb = manager.getCircuitBreaker("API_ST_REOPEN");
        recordFailures(cb, 4);

        Thread.sleep(1200);

        // HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 모두 실패해야 OPEN 으로 재전이된다.
        // Resilience4j 는 허용 호출 수(2회)를 모두 소진한 후에 실패율을 평가하므로
        // 1회만 실패하면 HALF_OPEN 상태가 유지된다.
        recordFailures(cb, DEFAULT_HALF_OPEN_PERMITS);

        assertEquals(CircuitBreaker.State.OPEN, cb.getState(),
                "HALF_OPEN 에서 허용 호출 수(2회) 모두 실패하면 다시 OPEN 으로 전이되어야 한다");
    }

    // =========================================================================
    // 4. reload
    // =========================================================================

    @Test
    @DisplayName("4-1. reload(key) → CircuitBreakerConfig 갱신 (waitDuration 1초 → 60초)")
    void testReload_key_updatesCircuitBreakerConfig() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-001", "API_RELOAD", "1")));
        manager.reload();

        // waitDurationInOpenState 60초로 변경
        TypedCircuitBreakerVO updated = TypedCircuitBreakerVO.builder()
                .id("uuid-RLD-001").name("API_RELOAD").type("COUNT_BASED")
                .failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4)
                .waitDurationInOpenState(60)
                .permittedNumberOfCallsInHalfOpenState(2)
                .slowCallRateThreshold(100).slowCallDurationThreshold(5)
                .useYn("1").build();
        when(mockDao.get("uuid-RLD-001")).thenReturn(updated);
        manager.reload("uuid-RLD-001");

        CircuitBreaker cb = manager.getCircuitBreaker("API_RELOAD");
        assertEquals(60,
                cb.getCircuitBreakerConfig().getWaitDurationInOpenState().getSeconds(),
                "reload 후 waitDurationInOpenState 가 60초로 갱신되어야 한다");
    }

    @Test
    @DisplayName("4-2. reload(key) useYn=0 → 맵에서 제거 → default fallback")
    void testReload_key_disabledVo_removesFromMap() throws Exception {
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-002", "API_RM", "1")));
        manager.reload();
        assertEquals("API_RM", manager.getCircuitBreaker("API_RM").getName());

        // useYn=0 으로 비활성화
        TypedCircuitBreakerVO disabled = TypedCircuitBreakerVO.builder()
                .id("uuid-RLD-002").name("API_RM").type("COUNT_BASED")
                .failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4)
                .waitDurationInOpenState(1).permittedNumberOfCallsInHalfOpenState(2)
                .slowCallRateThreshold(100).slowCallDurationThreshold(5)
                .useYn("0").build();
        when(mockDao.get("uuid-RLD-002")).thenReturn(disabled);
        manager.reload("uuid-RLD-002");

        CircuitBreaker cb = manager.getCircuitBreaker("API_RM");
        assertEquals("default", cb.getName(),
                "비활성화된 CB 는 맵에서 제거 후 default 로 fallback 되어야 한다");
    }

    @Test
    @DisplayName("4-3. reload() 전체 → CircuitBreakerRegistry 재생성으로 이전 CB 상태 초기화")
    void testReload_all_refreshesAllCircuitBreakers() throws Exception {
        // 초기: API_FULL_A 등록 후 OPEN 상태로 만들기
        when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-FULL-A", "API_FULL_A", "1")));
        manager.reload();

        CircuitBreaker cbA = manager.getCircuitBreaker("API_FULL_A");
        recordFailures(cbA, 4);
        assertEquals(CircuitBreaker.State.OPEN, cbA.getState(), "전체 reload 전 API_FULL_A 는 OPEN 이어야 한다");

        // 전체 reload → CircuitBreakerRegistry 재생성
        // init() 은 apiIdCBConfigMap 을 초기화하지 않으므로 API_FULL_A 는 맵에 남아있다.
        // 하지만 새 레지스트리를 생성하므로 CB 상태(OPEN)는 초기화(CLOSED)된다.
        when(mockDao.getList()).thenReturn(Arrays.asList(
                buildVo("uuid-FULL-A", "API_FULL_A", "1"),
                buildVo("uuid-FULL-B", "API_FULL_B", "1")));
        manager.reload();

        // 레지스트리 재생성으로 API_FULL_A CB 상태가 CLOSED 로 초기화되어야 한다
        CircuitBreaker cbAAfter = manager.getCircuitBreaker("API_FULL_A");
        assertEquals(CircuitBreaker.State.CLOSED, cbAAfter.getState(),
                "전체 reload 후 CircuitBreakerRegistry 재생성으로 이전 CB 상태가 CLOSED 로 초기화되어야 한다");

        // 새 apiId 도 정상 등록
        assertEquals("API_FULL_B", manager.getCircuitBreaker("API_FULL_B").getName(),
                "전체 reload 후 새 apiId 는 정상 등록되어야 한다");
    }

    // =========================================================================
    // 5. 생명주기
    // =========================================================================

    @Test
    @DisplayName("5-1. start() 후 isStarted() = true")
    void testIsStarted_afterStart_returnsTrue() {
        assertTrue(manager.isStarted(), "start() 후 isStarted() 는 true 여야 한다");
    }
}
