package com.eactive.eai.custom.inbound.processor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Properties;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;

import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.InflowTargetVO;
import com.eactive.eai.common.inflow.dual.DualBucket;
import com.eactive.eai.common.inflow.dual.DualCustomBucket;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.custom.inflow.ClientDualBucket;

/**
 * DJBRequestProcessor 단위 테스트.
 *
 * <p>RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다.
 * @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DJBRequestProcessor를 최초 로딩시킨다.
 *
 * <p>주의: DJBRequestProcessor를 필드 타입으로 선언하면 테스트 클래스 로딩 시 함께 로딩되어
 * @BeforeAll 실행 전에 NPE가 발생한다. 반드시 메서드 본문 내에서만 참조해야 한다.
 */
class DJBRequestProcessorTest {

    @BeforeAll
    static void setupMockApplicationContext() {
        ApplicationContext mockContext = mock(ApplicationContext.class);
        EAIServerManager mockServerManager = mock(EAIServerManager.class);
        when(mockContext.getBean(EAIServerManager.class)).thenReturn(mockServerManager);
        when(mockServerManager.getLocalServerName()).thenReturn("TEST-SERVER");
        when(mockServerManager.getGroupInstId()).thenReturn("TEST-INST");

        ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext);
    }

    private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit) {
        InflowTargetVO vo = new InflowTargetVO();
        vo.setName(name);
        vo.setThresholdPerSecond(perSec);
        vo.setThreshold(threshold);
        vo.setThresholdTimeUnit(unit);
        vo.setActivate(true);
        return vo;
    }

    private EAIMessage mockEaiMsg(String clientId) {
        EAIMessage msg = mock(EAIMessage.class);
        when(msg.getClientId()).thenReturn(clientId);
        return msg;
    }

    // -------------------------------------------------------------------------
    // checkAdapterInflow
    // -------------------------------------------------------------------------

    @Test
    void checkAdapterInflow_dualBucket_pass_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        DualBucket mockBucket = mock(DualBucket.class);
        when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(null);

        assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
        verify(mockBucket).isAdapterPassDetail("ADAPTER_A");
    }

    @Test
    void checkAdapterInflow_dualBucket_blockedPerSecond() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        DualBucket mockBucket = mock(DualBucket.class);
        when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);

        assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
                processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
    }

    @Test
    void checkAdapterInflow_dualBucket_blockedThreshold() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        DualBucket mockBucket = mock(DualBucket.class);
        when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD);

        assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD,
                processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
    }

    @Test
    void checkAdapterInflow_nonDualBucket_pass_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        Bucket mockBucket = mock(Bucket.class);
        when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(true);

        assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
    }

    @Test
    void checkAdapterInflow_nonDualBucket_blocked_returnsBlocked() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        Bucket mockBucket = mock(Bucket.class);
        when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(false);

        assertEquals("BLOCKED", processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
    }

    // -------------------------------------------------------------------------
    // checkInterfaceInflow
    // -------------------------------------------------------------------------

    @Test
    void checkInterfaceInflow_dualBucket_pass_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        DualBucket mockBucket = mock(DualBucket.class);
        when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(null);

        assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
        verify(mockBucket).isInterfacePassDetail("IF_001");
    }

    @Test
    void checkInterfaceInflow_dualBucket_blockedPerSecond() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        DualBucket mockBucket = mock(DualBucket.class);
        when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);

        assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
                processor.checkInterfaceInflow(mockBucket, "IF_001"));
    }

    @Test
    void checkInterfaceInflow_nonDualBucket_pass_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        Bucket mockBucket = mock(Bucket.class);
        when(mockBucket.isInterfacePass("IF_001")).thenReturn(true);

        assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
    }

    @Test
    void checkInterfaceInflow_nonDualBucket_blocked_returnsBlocked() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        Bucket mockBucket = mock(Bucket.class);
        when(mockBucket.isInterfacePass("IF_001")).thenReturn(false);

        assertEquals("BLOCKED", processor.checkInterfaceInflow(mockBucket, "IF_001"));
    }

    // -------------------------------------------------------------------------
    // checkCustomInflow — ClientDualBucket 기반
    // -------------------------------------------------------------------------

    @Test
    void checkCustomInflow_clientDualBucket_pass_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        ClientDualBucket mockBucket = mock(ClientDualBucket.class);
        when(mockBucket.isClientPassDetail("CLIENT_A")).thenReturn(null);

        assertNull(processor.checkCustomInflow(mockBucket, mockEaiMsg("CLIENT_A"), new Properties()));
        verify(mockBucket).isClientPassDetail("CLIENT_A");
    }

    @Test
    void checkCustomInflow_clientDualBucket_blocked() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        ClientDualBucket mockBucket = mock(ClientDualBucket.class);
        when(mockBucket.isClientPassDetail("CLIENT_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD);

        assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD,
                processor.checkCustomInflow(mockBucket, mockEaiMsg("CLIENT_A"), new Properties()));
    }

    @Test
    void checkCustomInflow_nonClientDualBucket_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        Bucket mockBucket = mock(Bucket.class);

        assertNull(processor.checkCustomInflow(mockBucket, mockEaiMsg("CLIENT_A"), new Properties()));
    }

    @Test
    void checkCustomInflow_blankClientId_returnsNull() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        ClientDualBucket mockBucket = mock(ClientDualBucket.class);

        assertNull(processor.checkCustomInflow(mockBucket, mockEaiMsg(""), new Properties()));
        verify(mockBucket, never()).isClientPassDetail(any());
    }

    // -------------------------------------------------------------------------
    // buildInflowTargetErrorMsg
    // -------------------------------------------------------------------------

    @Test
    void buildInflowTargetErrorMsg_perSecond_includesReqPerSec() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");

        String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_PER_SECOND);

        assertEquals("API 호출 한도 초과 [결제API: 30req/sec]", msg);
    }

    @Test
    void buildInflowTargetErrorMsg_threshold_includesReqPerUnit() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");

        String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);

        assertEquals("API 호출 한도 초과 [결제API: 1000req/MIN]", msg);
    }

    @Test
    void buildInflowTargetErrorMsg_blockedFallback_simpleFormat() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");

        String msg = processor.buildInflowTargetErrorMsg(vo, "BLOCKED");

        assertEquals("API 호출 한도 초과 [결제API]", msg);
    }

    @Test
    void buildInflowTargetErrorMsg_nullBlockedType_simpleFormat() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");

        String msg = processor.buildInflowTargetErrorMsg(vo, null);

        assertEquals("API 호출 한도 초과 [결제API]", msg);
    }

    @Test
    void buildInflowTargetErrorMsg_hourUnit() {
        DJBRequestProcessor processor = new DJBRequestProcessor();
        InflowTargetVO vo = makeVO("조회API", 10, 500, "HOUR");

        String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);

        assertEquals("API 호출 한도 초과 [조회API: 500req/HOUR]", msg);
    }
}
