package com.eactive.eai.common.inflow.dual;

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

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

import com.eactive.eai.common.inflow.CustomGroupBucket;
import com.eactive.eai.common.inflow.InflowGroupVO;
import com.eactive.eai.common.inflow.InflowTargetVO;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.local.LocalBucket;

/**
 * DualInflowControlManager 메트릭 단위 테스트.
 *
 * <p>어댑터/인터페이스/그룹 메트릭을 검증한다.
 * 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
 */
class DualInflowControlManagerMetricsTest {

    private DualInflowControlManager manager;

    @BeforeEach
    void setUp() {
        manager = new DualInflowControlManager();
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private InflowGroupVO makeGroupVO(String groupId, String groupName) {
        InflowGroupVO vo = new InflowGroupVO();
        vo.setGroupId(groupId);
        vo.setGroupName(groupName);
        vo.setActivate(true);
        return vo;
    }

    private LocalBucket stableBucket(long capacity) {
        return Bucket4j.builder()
                .addLimit(Bandwidth.simple(capacity, Duration.ofHours(1)))
                .build();
    }

    private LocalBucket exhaustedBucket(long capacity) {
        LocalBucket b = stableBucket(capacity);
        b.tryConsume(capacity);
        return b;
    }

    private CustomGroupBucket passBucket(String groupId) {
        return new CustomGroupBucket(stableBucket(100), null, makeGroupVO(groupId, groupId + "-name"));
    }

    private CustomGroupBucket blockedPerSecondBucket(String groupId) {
        return new CustomGroupBucket(exhaustedBucket(1), null, makeGroupVO(groupId, groupId + "-name"));
    }

    private CustomGroupBucket blockedThresholdBucket(String groupId) {
        return new CustomGroupBucket(stableBucket(100), exhaustedBucket(1), makeGroupVO(groupId, groupId + "-name"));
    }

    private InflowTargetVO makeTargetVO(String name) {
        InflowTargetVO vo = new InflowTargetVO();
        vo.setName(name);
        vo.setThresholdPerSecond(100);
        vo.setThreshold(10000);
        vo.setThresholdTimeUnit("DAY");
        vo.setActivate(true);
        return vo;
    }

    private DualCustomBucket passDualBucket(String name) {
        return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
    }

    private DualCustomBucket blockedPerSecondDualBucket(String name) {
        return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
    }

    private DualCustomBucket blockedThresholdDualBucket(String name) {
        return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
    }

    private void injectAdapterMap(Map<String, DualCustomBucket> map) {
        ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
    }

    private void injectInterfaceMap(Map<String, DualCustomBucket> map) {
        ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
    }

    private void injectGroupBucketList(Map<String, CustomGroupBucket> map) {
        ReflectionTestUtils.setField(manager, "groupBucketList", map);
    }

    // =========================================================================
    // TargetMetrics 내부 클래스
    // =========================================================================

    @Test
    void targetMetrics_initialState_allCountersZero() {
        DualInflowControlManager.TargetMetrics m = new DualInflowControlManager.TargetMetrics();

        assertEquals(0, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
        assertTrue(m.lastResetTime > 0);
    }

    @Test
    void targetMetrics_reset_countersZeroAndResetTimeUpdated() throws InterruptedException {
        DualInflowControlManager.TargetMetrics m = new DualInflowControlManager.TargetMetrics();
        m.allowed.set(50);
        m.rejectedPerSecond.set(10);
        m.rejectedThreshold.set(5);
        long before = System.currentTimeMillis();

        Thread.sleep(1);
        m.reset();

        assertEquals(0, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
        assertTrue(m.lastResetTime >= before);
    }

    // =========================================================================
    // isGroupPass() — 카운터 증가
    // =========================================================================

    @Test
    void isGroupPass_groupNotInBucketList_incrementsAllowedAndReturnsNull() {
        String result = manager.isGroupPass("UNKNOWN_G");

        assertNull(result);
        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("UNKNOWN_G");
        assertNotNull(m);
        assertEquals(1, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void isGroupPass_pass_incrementsAllowed() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        map.put("G1", passBucket("G1"));
        injectGroupBucketList(map);

        String result = manager.isGroupPass("G1");

        assertNull(result);
        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("G1");
        assertEquals(1, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void isGroupPass_blockedPerSecond_incrementsRejectedPerSecond() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        map.put("G1", blockedPerSecondBucket("G1"));
        injectGroupBucketList(map);

        String result = manager.isGroupPass("G1");

        assertEquals(CustomGroupBucket.RESULT_BLOCKED_PER_SECOND, result);
        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("G1");
        assertEquals(0, m.allowed.get());
        assertEquals(1, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void isGroupPass_blockedThreshold_incrementsRejectedThreshold() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        map.put("G1", blockedThresholdBucket("G1"));
        injectGroupBucketList(map);

        String result = manager.isGroupPass("G1");

        assertEquals(CustomGroupBucket.RESULT_BLOCKED_THRESHOLD, result);
        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("G1");
        assertEquals(0, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(1, m.rejectedThreshold.get());
    }

    @Test
    void isGroupPass_multipleCalls_countersAccumulate() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        LocalBucket b = stableBucket(5);
        map.put("G1", new CustomGroupBucket(b, null, makeGroupVO("G1", "G1-name")));
        injectGroupBucketList(map);

        for (int i = 0; i < 8; i++) {
            manager.isGroupPass("G1");
        }

        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("G1");
        assertEquals(5, m.allowed.get());
        assertEquals(3, m.rejectedPerSecond.get());
        assertEquals(8, m.allowed.get() + m.rejectedPerSecond.get() + m.rejectedThreshold.get());
    }

    @Test
    void isGroupPass_differentGroups_independentMetrics() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        map.put("G1", passBucket("G1"));
        map.put("G2", blockedPerSecondBucket("G2"));
        injectGroupBucketList(map);

        manager.isGroupPass("G1");
        manager.isGroupPass("G1");
        manager.isGroupPass("G2");

        assertEquals(2, manager.getGroupMetrics("G1").allowed.get());
        assertEquals(0, manager.getGroupMetrics("G1").rejectedPerSecond.get());
        assertEquals(0, manager.getGroupMetrics("G2").allowed.get());
        assertEquals(1, manager.getGroupMetrics("G2").rejectedPerSecond.get());
    }

    // =========================================================================
    // getGroupMetrics / getAllGroupMetrics
    // =========================================================================

    @Test
    void getGroupMetrics_beforeAnyCall_returnsNull() {
        assertNull(manager.getGroupMetrics("G1"));
    }

    @Test
    void getGroupMetrics_afterCall_returnsNonNull() {
        manager.isGroupPass("G1");
        assertNotNull(manager.getGroupMetrics("G1"));
    }

    @Test
    void getAllGroupMetrics_empty_returnsEmptyMap() {
        assertTrue(manager.getAllGroupMetrics().isEmpty());
    }

    @Test
    void getAllGroupMetrics_afterMultipleGroups_containsAll() {
        manager.isGroupPass("G1");
        manager.isGroupPass("G2");
        manager.isGroupPass("G3");

        Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllGroupMetrics();
        assertEquals(3, all.size());
        assertTrue(all.containsKey("G1"));
        assertTrue(all.containsKey("G2"));
        assertTrue(all.containsKey("G3"));
    }

    @Test
    void getAllGroupMetrics_returnsUnmodifiableView() {
        manager.isGroupPass("G1");

        Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllGroupMetrics();
        assertThrows(UnsupportedOperationException.class, () -> all.put("NEW", null));
    }

    // =========================================================================
    // resetGroupMetrics / resetAllGroupMetrics
    // =========================================================================

    @Test
    void resetGroupMetrics_resetsTargetGroup() {
        Map<String, CustomGroupBucket> map = new ConcurrentHashMap<>();
        map.put("G1", passBucket("G1"));
        injectGroupBucketList(map);

        manager.isGroupPass("G1");
        manager.isGroupPass("G1");
        assertEquals(2, manager.getGroupMetrics("G1").allowed.get());

        manager.resetGroupMetrics("G1");

        DualInflowControlManager.TargetMetrics m = manager.getGroupMetrics("G1");
        assertEquals(0, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void resetGroupMetrics_updatesLastResetTime() throws InterruptedException {
        manager.isGroupPass("G1");
        long timeBefore = manager.getGroupMetrics("G1").lastResetTime;

        Thread.sleep(1);
        manager.resetGroupMetrics("G1");

        assertTrue(manager.getGroupMetrics("G1").lastResetTime >= timeBefore);
    }

    @Test
    void resetGroupMetrics_nonExistentGroup_noException() {
        assertDoesNotThrow(() -> manager.resetGroupMetrics("NON_EXISTENT"));
    }

    @Test
    void resetGroupMetrics_doesNotAffectOtherGroups() {
        manager.isGroupPass("G1");
        manager.isGroupPass("G2");

        manager.resetGroupMetrics("G1");

        assertEquals(0, manager.getGroupMetrics("G1").allowed.get());
        assertEquals(1, manager.getGroupMetrics("G2").allowed.get());
    }

    @Test
    void resetAllGroupMetrics_resetsAllGroups() {
        manager.isGroupPass("G1");
        manager.isGroupPass("G1");
        manager.isGroupPass("G2");

        manager.resetAllGroupMetrics();

        assertEquals(0, manager.getGroupMetrics("G1").allowed.get());
        assertEquals(0, manager.getGroupMetrics("G2").allowed.get());
    }

    @Test
    void resetAllGroupMetrics_emptyMap_noException() {
        assertDoesNotThrow(() -> manager.resetAllGroupMetrics());
    }

    @Test
    void resetAllGroupMetrics_preservesKeys() {
        manager.isGroupPass("G1");
        manager.isGroupPass("G2");

        manager.resetAllGroupMetrics();

        assertNotNull(manager.getGroupMetrics("G1"));
        assertNotNull(manager.getGroupMetrics("G2"));
    }

    // =========================================================================
    // isAdapterPassDetail() — 카운터 증가
    // =========================================================================

    @Test
    void isAdapterPassDetail_bucketNotRegistered_noMetricsCreated() {
        manager.isAdapterPassDetail("UNKNOWN");

        assertNull(manager.getAdapterMetrics("UNKNOWN"));
    }

    @Test
    void isAdapterPassDetail_pass_incrementsAllowed() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", passDualBucket("A1"));
        injectAdapterMap(map);

        String result = manager.isAdapterPassDetail("A1");

        assertNull(result);
        DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
        assertNotNull(m);
        assertEquals(1, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void isAdapterPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", blockedPerSecondDualBucket("A1"));
        injectAdapterMap(map);

        String result = manager.isAdapterPassDetail("A1");

        assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, result);
        DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
        assertEquals(0, m.allowed.get());
        assertEquals(1, m.rejectedPerSecond.get());
        assertEquals(0, m.rejectedThreshold.get());
    }

    @Test
    void isAdapterPassDetail_blockedThreshold_incrementsRejectedThreshold() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", blockedThresholdDualBucket("A1"));
        injectAdapterMap(map);

        String result = manager.isAdapterPassDetail("A1");

        assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, result);
        DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
        assertEquals(0, m.allowed.get());
        assertEquals(0, m.rejectedPerSecond.get());
        assertEquals(1, m.rejectedThreshold.get());
    }

    @Test
    void isAdapterPassDetail_multipleCalls_countersAccumulate() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        LocalBucket b = stableBucket(3);
        map.put("A1", new DualCustomBucket(b, null, makeTargetVO("A1")));
        injectAdapterMap(map);

        for (int i = 0; i < 5; i++) manager.isAdapterPassDetail("A1");

        DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
        assertEquals(3, m.allowed.get());
        assertEquals(2, m.rejectedPerSecond.get());
        assertEquals(5, m.allowed.get() + m.rejectedPerSecond.get() + m.rejectedThreshold.get());
    }

    // =========================================================================
    // isInterfacePassDetail() — 카운터 증가
    // =========================================================================

    @Test
    void isInterfacePassDetail_bucketNotRegistered_noMetricsCreated() {
        manager.isInterfacePassDetail("UNKNOWN");

        assertNull(manager.getInterfaceMetrics("UNKNOWN"));
    }

    @Test
    void isInterfacePassDetail_pass_incrementsAllowed() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF1", passDualBucket("IF1"));
        injectInterfaceMap(map);

        manager.isInterfacePassDetail("IF1");

        assertEquals(1, manager.getInterfaceMetrics("IF1").allowed.get());
    }

    @Test
    void isInterfacePassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF1", blockedPerSecondDualBucket("IF1"));
        injectInterfaceMap(map);

        manager.isInterfacePassDetail("IF1");

        assertEquals(1, manager.getInterfaceMetrics("IF1").rejectedPerSecond.get());
    }

    @Test
    void isInterfacePassDetail_differentInterfaces_independentMetrics() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF1", passDualBucket("IF1"));
        map.put("IF2", blockedPerSecondDualBucket("IF2"));
        injectInterfaceMap(map);

        manager.isInterfacePassDetail("IF1");
        manager.isInterfacePassDetail("IF1");
        manager.isInterfacePassDetail("IF2");

        assertEquals(2, manager.getInterfaceMetrics("IF1").allowed.get());
        assertEquals(1, manager.getInterfaceMetrics("IF2").rejectedPerSecond.get());
    }

    // =========================================================================
    // getAllXxxMetrics — 조회 및 불변성
    // =========================================================================

    @Test
    void getAllAdapterMetrics_afterCalls_containsEntries() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", passDualBucket("A1"));
        map.put("A2", passDualBucket("A2"));
        injectAdapterMap(map);

        manager.isAdapterPassDetail("A1");
        manager.isAdapterPassDetail("A2");

        Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllAdapterMetrics();
        assertEquals(2, all.size());
        assertTrue(all.containsKey("A1"));
        assertTrue(all.containsKey("A2"));
    }

    @Test
    void getAllAdapterMetrics_returnsUnmodifiableView() {
        Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllAdapterMetrics();
        assertThrows(UnsupportedOperationException.class, () -> all.put("NEW", null));
    }

    @Test
    void getAllInterfaceMetrics_empty_returnsEmptyMap() {
        assertTrue(manager.getAllInterfaceMetrics().isEmpty());
    }

    // =========================================================================
    // resetXxxMetrics / resetAllXxxMetrics
    // =========================================================================

    @Test
    void resetAdapterMetrics_resetsTargetAdapter() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", passDualBucket("A1"));
        map.put("A2", passDualBucket("A2"));
        injectAdapterMap(map);

        manager.isAdapterPassDetail("A1");
        manager.isAdapterPassDetail("A1");
        manager.isAdapterPassDetail("A2");

        manager.resetAdapterMetrics("A1");

        assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
        assertEquals(1, manager.getAdapterMetrics("A2").allowed.get());
    }

    @Test
    void resetAdapterMetrics_nonExistent_noException() {
        assertDoesNotThrow(() -> manager.resetAdapterMetrics("NONE"));
    }

    @Test
    void resetAllAdapterMetrics_resetsAll() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", passDualBucket("A1"));
        map.put("A2", passDualBucket("A2"));
        injectAdapterMap(map);

        manager.isAdapterPassDetail("A1");
        manager.isAdapterPassDetail("A2");
        manager.resetAllAdapterMetrics();

        assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
        assertEquals(0, manager.getAdapterMetrics("A2").allowed.get());
    }

    @Test
    void resetInterfaceMetrics_resetsTargetInterface() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF1", passDualBucket("IF1"));
        injectInterfaceMap(map);

        manager.isInterfacePassDetail("IF1");
        manager.isInterfacePassDetail("IF1");
        manager.resetInterfaceMetrics("IF1");

        assertEquals(0, manager.getInterfaceMetrics("IF1").allowed.get());
    }

    @Test
    void resetAllInterfaceMetrics_emptyMap_noException() {
        assertDoesNotThrow(() -> manager.resetAllInterfaceMetrics());
    }

    // =========================================================================
    // remove 시 메트릭 자동 삭제
    // =========================================================================

    @Test
    void removeAdapter_cleansUpMetrics() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("A1", passDualBucket("A1"));
        ReflectionTestUtils.setField(manager, "adapterBucketMap", map);

        manager.isAdapterPassDetail("A1");
        assertNotNull(manager.getAdapterMetrics("A1"));

        manager.removeAdapter("A1");

        assertNull(manager.getAdapterMetrics("A1"));
    }

    @Test
    void removeInterface_cleansUpMetrics() {
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF1", passDualBucket("IF1"));
        ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);

        manager.isInterfacePassDetail("IF1");
        assertNotNull(manager.getInterfaceMetrics("IF1"));

        manager.removeInterface("IF1");

        assertNull(manager.getInterfaceMetrics("IF1"));
    }
}
