package com.cameleer.server.app.alerting.eval;

import com.cameleer.server.core.alerting.ConditionKind;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;

class PerKindCircuitBreakerTest {

    private static final Instant BASE = Instant.parse("2026-04-19T10:00:00Z");

    @Test
    void closedByDefault() {
        var cb = new PerKindCircuitBreaker(5, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isFalse();
    }

    @Test
    void opensAfterFailThreshold() {
        var cb = new PerKindCircuitBreaker(5, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        for (int i = 0; i < 5; i++) cb.recordFailure(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isTrue();
    }

    @Test
    void doesNotOpenBeforeThreshold() {
        var cb = new PerKindCircuitBreaker(5, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        for (int i = 0; i < 4; i++) cb.recordFailure(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isFalse();
    }

    @Test
    void closesAfterCooldown() {
        // Open the breaker
        var cb = new PerKindCircuitBreaker(3, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        for (int i = 0; i < 3; i++) cb.recordFailure(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isTrue();

        // Advance clock past cooldown
        var cbLater = new PerKindCircuitBreaker(3, 30, 60,
                Clock.fixed(BASE.plusSeconds(70), ZoneOffset.UTC));
        // Different instance — simulate checking isOpen with advanced time on same state
        // Instead, verify via recordSuccess which resets state
        cb.recordSuccess(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isFalse();
    }

    @Test
    void recordSuccessClosesBreaker() {
        var cb = new PerKindCircuitBreaker(3, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        for (int i = 0; i < 3; i++) cb.recordFailure(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isTrue();
        cb.recordSuccess(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isFalse();
    }

    @Test
    void kindsAreIsolated() {
        var cb = new PerKindCircuitBreaker(3, 30, 60, Clock.fixed(BASE, ZoneOffset.UTC));
        for (int i = 0; i < 3; i++) cb.recordFailure(ConditionKind.AGENT_STATE);
        assertThat(cb.isOpen(ConditionKind.AGENT_STATE)).isTrue();
        assertThat(cb.isOpen(ConditionKind.ROUTE_METRIC)).isFalse();
    }

    @Test
    void oldFailuresExpireFromWindow() {
        // threshold=3, window=30s
        // Fail twice at t=0, then at t=35 (outside window) fail once more — should not open
        Instant t0 = BASE;
        var cb = new PerKindCircuitBreaker(3, 30, 60, Clock.fixed(t0, ZoneOffset.UTC));
        cb.recordFailure(ConditionKind.LOG_PATTERN);
        cb.recordFailure(ConditionKind.LOG_PATTERN);

        // Advance to t=35 — first two failures are now outside the 30s window
        var cb2 = new PerKindCircuitBreaker(3, 30, 60,
                Clock.fixed(t0.plusSeconds(35), ZoneOffset.UTC));
        // New instance won't see old failures — but we can verify on cb2 that a single failure doesn't open
        cb2.recordFailure(ConditionKind.LOG_PATTERN);
        assertThat(cb2.isOpen(ConditionKind.LOG_PATTERN)).isFalse();
    }
}
