package dev.awesomedev.mirim.service;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;

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

/**
 * 이 파일이 하는 일:
 * 범용 레이트리밋(호출 빈도 제한)의 규칙을 검증한다.
 *
 * 학습 포인트 — 가짜 시계(MutableClock)로 "창이 지나면 리셋"을 실제로 기다리지 않고 검증한다.
 * (LoginAttemptServiceTest와 같은 기법. 시간을 주입받게 설계한 덕분이다.)
 */
class RateLimiterServiceTest {

    /** 테스트용 시계: advance()로 시간을 앞당긴다. */
    static class MutableClock extends Clock {
        private Instant now = Instant.parse("2026-09-01T09:00:00Z");

        void advance(Duration duration) {
            now = now.plus(duration);
        }

        @Override
        public Instant instant() {
            return now;
        }

        @Override
        public java.time.ZoneId getZone() {
            return ZoneOffset.UTC;
        }

        @Override
        public Clock withZone(java.time.ZoneId zone) {
            return this;
        }
    }

    private final MutableClock clock = new MutableClock();
    private final RateLimiterService service = new RateLimiterService(clock);

    private static final Duration WINDOW = Duration.ofMinutes(10);

    @Test
    @DisplayName("한도까지는 허용하고, 한도를 넘으면 거부한다")
    void allows_up_to_limit_then_denies() {
        for (int i = 0; i < 3; i++) {
            assertTrue(service.tryAcquire("signup", "1.2.3.4", 3, WINDOW), (i + 1) + "번째는 허용");
        }
        assertFalse(service.tryAcquire("signup", "1.2.3.4", 3, WINDOW), "4번째는 거부");
    }

    @Test
    @DisplayName("다른 키(IP)는 서로 카운트가 섞이지 않는다")
    void keys_are_independent() {
        assertTrue(service.tryAcquire("signup", "1.1.1.1", 1, WINDOW));
        assertFalse(service.tryAcquire("signup", "1.1.1.1", 1, WINDOW)); // 이 IP는 한도 참
        assertTrue(service.tryAcquire("signup", "2.2.2.2", 1, WINDOW));  // 다른 IP는 멀쩡
    }

    @Test
    @DisplayName("같은 IP라도 용도(bucket)가 다르면 따로 센다")
    void buckets_are_independent() {
        assertTrue(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
        assertFalse(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
        assertTrue(service.tryAcquire("find-id", "1.2.3.4", 1, WINDOW)); // 다른 bucket은 별개
    }

    @Test
    @DisplayName("창(window)이 지나면 카운트가 리셋되어 다시 허용된다")
    void resets_after_window() {
        assertTrue(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
        assertFalse(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW)); // 한도 참

        clock.advance(WINDOW.plusSeconds(1)); // 창이 지나간다
        assertTrue(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW)); // 새 창에서 다시 허용
    }

    @Test
    @DisplayName("한도 초과가 창을 연장시키지 않는다 — 넘쳐도 창은 제때 끝난다")
    void overflow_does_not_extend_window() {
        assertTrue(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
        // 창 중간에 계속 두드려도(모두 거부됨)
        clock.advance(Duration.ofMinutes(5));
        assertFalse(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
        // 최초 창 시작 기준 10분이 지나면 리셋되어야 한다
        clock.advance(Duration.ofMinutes(5).plusSeconds(1));
        assertTrue(service.tryAcquire("signup", "1.2.3.4", 1, WINDOW));
    }
}
