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 java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

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

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

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

/**
 * DualCustomBucket / DualInflowControlManager 동시성 테스트.
 *
 * <p>모든 버킷을 Duration.ofHours(1)로 생성하여 테스트 실행 중 리필이 없도록 한다.
 * CountDownLatch로 모든 스레드를 동시 출발시켜 race window를 최대화한다.
 */
class DualInflowControlConcurrencyTest {

    private static final int THREAD_COUNT    = 20;
    private static final int TRIES_PER_THREAD = 50;
    private static final int TOTAL_TRIES     = THREAD_COUNT * TRIES_PER_THREAD; // 1000

    private DualInflowControlManager manager;

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

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

    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 LocalBucket stableBucket(long capacity) {
        return Bucket4j.builder()
                .addLimit(Bandwidth.simple(capacity, Duration.ofHours(1)))
                .build();
    }

    private boolean runConcurrently(int threads, int triesPerThread, Runnable work)
            throws InterruptedException {
        CountDownLatch ready = new CountDownLatch(threads);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done  = new CountDownLatch(threads);
        ExecutorService executor = Executors.newFixedThreadPool(threads);

        for (int i = 0; i < threads; i++) {
            executor.submit(() -> {
                ready.countDown();
                try {
                    start.await();
                    for (int j = 0; j < triesPerThread; j++) {
                        work.run();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            });
        }

        ready.await();
        start.countDown();
        boolean completed = done.await(30, TimeUnit.SECONDS);
        executor.shutdown();
        return completed;
    }

    // =========================================================================
    // DualCustomBucket 동시성 테스트
    // =========================================================================

    @Test
    void perSecondOnly_totalPassEqualsCapacity() throws InterruptedException {
        long capacity = 200;
        DualCustomBucket bucket = new DualCustomBucket(
                stableBucket(capacity), null, makeVO("A", capacity, 0, null));

        AtomicInteger passCount        = new AtomicInteger();
        AtomicInteger blockedPerSecond = new AtomicInteger();
        AtomicInteger blockedThreshold = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = bucket.tryConsume();
            if (result == null)                                                   passCount.incrementAndGet();
            else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result))  blockedPerSecond.incrementAndGet();
            else                                                                  blockedThreshold.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockedPerSecond.get() + blockedThreshold.get());
        assertEquals(capacity, passCount.get());
        assertEquals(0, blockedThreshold.get());
    }

    @Test
    void thresholdOnly_totalPassEqualsCapacity() throws InterruptedException {
        long capacity = 200;
        DualCustomBucket bucket = new DualCustomBucket(
                null, stableBucket(capacity), makeVO("A", 0, capacity, "HOUR"));

        AtomicInteger passCount        = new AtomicInteger();
        AtomicInteger blockedThreshold = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = bucket.tryConsume();
            if (result == null)                                                    passCount.incrementAndGet();
            else if (DualCustomBucket.RESULT_BLOCKED_THRESHOLD.equals(result))    blockedThreshold.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockedThreshold.get());
        assertEquals(capacity, passCount.get());
    }

    @Test
    void dualBuckets_thresholdIsBottleneck_totalPassEqualsThreshold() throws InterruptedException {
        long perSecCapacity = 2000;
        long threshCapacity = 200;

        DualCustomBucket bucket = new DualCustomBucket(
                stableBucket(perSecCapacity), stableBucket(threshCapacity),
                makeVO("A", perSecCapacity, threshCapacity, "HOUR"));

        AtomicInteger passCount        = new AtomicInteger();
        AtomicInteger blockedPerSecond = new AtomicInteger();
        AtomicInteger blockedThreshold = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = bucket.tryConsume();
            if (result == null)                                                    passCount.incrementAndGet();
            else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result))   blockedPerSecond.incrementAndGet();
            else                                                                   blockedThreshold.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockedPerSecond.get() + blockedThreshold.get());
        assertEquals(threshCapacity, passCount.get());
        assertEquals(0, blockedPerSecond.get());
    }

    @Test
    void dualBuckets_perSecondIsBottleneck_noThresholdTokenLeak() throws InterruptedException {
        long perSecCapacity = 100;
        long threshCapacity = 2000;

        DualCustomBucket bucket = new DualCustomBucket(
                stableBucket(perSecCapacity), stableBucket(threshCapacity),
                makeVO("A", perSecCapacity, threshCapacity, "HOUR"));

        AtomicInteger passCount        = new AtomicInteger();
        AtomicInteger blockedPerSecond = new AtomicInteger();
        AtomicInteger blockedThreshold = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = bucket.tryConsume();
            if (result == null)                                                    passCount.incrementAndGet();
            else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result))   blockedPerSecond.incrementAndGet();
            else                                                                   blockedThreshold.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockedPerSecond.get() + blockedThreshold.get());
        assertEquals(perSecCapacity, passCount.get());
        assertEquals(0, blockedThreshold.get());
        assertEquals(perSecCapacity, threshCapacity - bucket.getThresholdAvailableTokens());
    }

    @Test
    void availableTokensConsistentAfterConcurrentConsume() throws InterruptedException {
        long capacity = 300;
        DualCustomBucket bucket = new DualCustomBucket(
                stableBucket(capacity), null, makeVO("A", capacity, 0, null));

        AtomicInteger passCount = new AtomicInteger();
        runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            if (bucket.tryConsume() == null) passCount.incrementAndGet();
        });

        long remaining = bucket.getPerSecondAvailableTokens();
        assertEquals(capacity, passCount.get() + remaining);
    }

    // =========================================================================
    // DualInflowControlManager 동시성 테스트
    // =========================================================================

    @Test
    void manager_isAdapterPassDetail_concurrent_totalPassEqualsCapacity() throws InterruptedException {
        long capacity = 200;
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("ADAPTER_A", new DualCustomBucket(
                stableBucket(capacity), null, makeVO("ADAPTER_A", capacity, 0, null)));
        ReflectionTestUtils.setField(manager, "adapterBucketMap", map);

        AtomicInteger passCount  = new AtomicInteger();
        AtomicInteger blockCount = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = manager.isAdapterPassDetail("ADAPTER_A");
            if (result == null) passCount.incrementAndGet();
            else                blockCount.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockCount.get());
        assertEquals(capacity, passCount.get());
    }

    @Test
    void manager_isInterfacePassDetail_concurrent_totalPassEqualsCapacity() throws InterruptedException {
        long capacity = 200;
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("IF_001", new DualCustomBucket(
                null, stableBucket(capacity), makeVO("IF_001", 0, capacity, "HOUR")));
        ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);

        AtomicInteger passCount  = new AtomicInteger();
        AtomicInteger blockCount = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            String result = manager.isInterfacePassDetail("IF_001");
            if (result == null) passCount.incrementAndGet();
            else                blockCount.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockCount.get());
        assertEquals(capacity, passCount.get());
    }

    @Test
    void manager_volatileMapSwap_noConcurrentException() throws InterruptedException {
        String adapterName = "ADAPTER_A";
        int readerCount = THREAD_COUNT;
        int swapCount   = 200;

        Map<String, DualCustomBucket> initialMap = new ConcurrentHashMap<>();
        initialMap.put(adapterName, new DualCustomBucket(
                stableBucket(10_000), null, makeVO(adapterName, 10_000, 0, null)));
        ReflectionTestUtils.setField(manager, "adapterBucketMap", initialMap);

        AtomicReference<Throwable> error = new AtomicReference<>();
        CountDownLatch ready = new CountDownLatch(readerCount + 1);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done  = new CountDownLatch(readerCount + 1);
        ExecutorService executor = Executors.newFixedThreadPool(readerCount + 1);

        for (int i = 0; i < readerCount; i++) {
            executor.submit(() -> {
                ready.countDown();
                try {
                    start.await();
                    for (int j = 0; j < TRIES_PER_THREAD; j++) {
                        manager.isAdapterPassDetail(adapterName);
                    }
                } catch (Throwable t) {
                    error.compareAndSet(null, t);
                } finally {
                    done.countDown();
                }
            });
        }

        executor.submit(() -> {
            ready.countDown();
            try {
                start.await();
                for (int i = 0; i < swapCount; i++) {
                    Map<String, DualCustomBucket> newMap = new ConcurrentHashMap<>();
                    newMap.put(adapterName, new DualCustomBucket(
                            stableBucket(10_000), null, makeVO(adapterName, 10_000, 0, null)));
                    ReflectionTestUtils.setField(manager, "adapterBucketMap", newMap);
                }
            } catch (Throwable t) {
                error.compareAndSet(null, t);
            } finally {
                done.countDown();
            }
        });

        ready.await();
        start.countDown();
        boolean completed = done.await(30, TimeUnit.SECONDS);
        executor.shutdown();

        assertTrue(completed, "테스트가 타임아웃되었습니다");
        assertNull(error.get(), error.get() != null ? "예외 발생: " + error.get() : null);
    }

    @Test
    void manager_mixedPassAndDetailCalls_totalPassConsistent() throws InterruptedException {
        long capacity = 300;
        Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
        map.put("ADAPTER_A", new DualCustomBucket(
                stableBucket(capacity), null, makeVO("ADAPTER_A", capacity, 0, null)));
        ReflectionTestUtils.setField(manager, "adapterBucketMap", map);

        AtomicInteger passCount  = new AtomicInteger();
        AtomicInteger blockCount = new AtomicInteger();
        AtomicInteger callIndex  = new AtomicInteger();

        boolean done = runConcurrently(THREAD_COUNT, TRIES_PER_THREAD, () -> {
            boolean useDetail = (callIndex.getAndIncrement() % 2 == 0);
            boolean passed;
            if (useDetail) {
                passed = (manager.isAdapterPassDetail("ADAPTER_A") == null);
            } else {
                passed = manager.isAdapterPass("ADAPTER_A");
            }
            if (passed) passCount.incrementAndGet();
            else        blockCount.incrementAndGet();
        });

        assertTrue(done, "테스트가 타임아웃되었습니다");
        assertEquals(TOTAL_TRIES, passCount.get() + blockCount.get());
        assertEquals(capacity, passCount.get());
    }
}
