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

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

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;

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

class AlertStateTransitionsTest {

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

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

    private AlertRule ruleWith(int forDurationSeconds) {
        return new AlertRule(
                UUID.randomUUID(), UUID.randomUUID(), "test-rule", null,
                AlertSeverity.WARNING, true, ConditionKind.AGENT_STATE,
                new AgentStateCondition(new AlertScope(null, null, null), "DEAD", 60),
                60, forDurationSeconds, 60,
                "{{rule.name}} fired", "Alert: {{alert.state}}",
                List.of(), List.of(),
                NOW, null, null, Map.of(),
                NOW, "u1", NOW, "u1");
    }

    private AlertInstance openInstance(AlertState state, Instant firedAt, String ackedBy) {
        return new AlertInstance(
                UUID.randomUUID(), UUID.randomUUID(), Map.of(), UUID.randomUUID(),
                state, AlertSeverity.WARNING,
                firedAt, null, ackedBy, null, null, null, null, false,
                1.0, null, Map.of(), "title", "msg",
                List.of(), List.of(), List.of());
    }

    private static final EvalResult.Firing FIRING_RESULT =
            new EvalResult.Firing(2500.0, 2000.0, Map.of());

    // -------------------------------------------------------------------------
    // Clear branch
    // -------------------------------------------------------------------------

    @Test
    void clearWithNoOpenInstanceIsNoOp() {
        var next = AlertStateTransitions.apply(null, EvalResult.Clear.INSTANCE, ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }

    @Test
    void clearWithAlreadyResolvedInstanceIsNoOp() {
        var resolved = openInstance(AlertState.RESOLVED, NOW.minusSeconds(120), null);
        var next = AlertStateTransitions.apply(resolved, EvalResult.Clear.INSTANCE, ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }

    @Test
    void firingClearTransitionsToResolved() {
        var firing = openInstance(AlertState.FIRING, NOW.minusSeconds(90), null);
        var next = AlertStateTransitions.apply(firing, EvalResult.Clear.INSTANCE, ruleWith(0), NOW);
        assertThat(next).hasValueSatisfying(i -> {
            assertThat(i.state()).isEqualTo(AlertState.RESOLVED);
            assertThat(i.resolvedAt()).isEqualTo(NOW);
        });
    }

    @Test
    void ackedInstanceClearsToResolved() {
        var acked = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null)
                .withAck("alice", Instant.parse("2026-04-19T11:55:00Z"));
        var next = AlertStateTransitions.apply(acked, EvalResult.Clear.INSTANCE, ruleWith(0), NOW);
        assertThat(next).hasValueSatisfying(i -> {
            assertThat(i.state()).isEqualTo(AlertState.RESOLVED);
            assertThat(i.resolvedAt()).isEqualTo(NOW);
            assertThat(i.ackedBy()).isEqualTo("alice");  // preserves acked_by
        });
    }

    // -------------------------------------------------------------------------
    // Firing branch — no open instance
    // -------------------------------------------------------------------------

    @Test
    void firingWithNoOpenInstanceCreatesPendingIfForDuration() {
        var rule = ruleWith(60);
        var next = AlertStateTransitions.apply(null, FIRING_RESULT, rule, NOW);
        assertThat(next).hasValueSatisfying(i -> {
            assertThat(i.state()).isEqualTo(AlertState.PENDING);
            assertThat(i.firedAt()).isEqualTo(NOW);
            assertThat(i.ruleId()).isEqualTo(rule.id());
        });
    }

    @Test
    void firingWithNoForDurationGoesStraightToFiring() {
        var rule = ruleWith(0);
        var next = AlertStateTransitions.apply(null, new EvalResult.Firing(1.0, null, Map.of()), rule, NOW);
        assertThat(next).hasValueSatisfying(i -> {
            assertThat(i.state()).isEqualTo(AlertState.FIRING);
            assertThat(i.firedAt()).isEqualTo(NOW);
        });
    }

    // -------------------------------------------------------------------------
    // Firing branch — PENDING current
    // -------------------------------------------------------------------------

    @Test
    void pendingStaysWhenForDurationNotElapsed() {
        var rule = ruleWith(60);
        // firedAt = NOW-10s, forDuration=60s → promoteAt = NOW+50s → still in window
        var pending = openInstance(AlertState.PENDING, NOW.minusSeconds(10), null);
        var next = AlertStateTransitions.apply(pending, FIRING_RESULT, rule, NOW);
        assertThat(next).isEmpty();  // no change
    }

    @Test
    void pendingPromotesToFiringAfterForDuration() {
        var rule = ruleWith(60);
        // firedAt = NOW-120s, forDuration=60s → promoteAt = NOW-60s → elapsed
        var pending = openInstance(AlertState.PENDING, NOW.minusSeconds(120), null);
        var next = AlertStateTransitions.apply(pending, FIRING_RESULT, rule, NOW);
        assertThat(next).hasValueSatisfying(i -> {
            assertThat(i.state()).isEqualTo(AlertState.FIRING);
            assertThat(i.firedAt()).isEqualTo(NOW);
        });
    }

    // -------------------------------------------------------------------------
    // Firing branch — already open FIRING (with or without ack)
    // -------------------------------------------------------------------------

    @Test
    void firingWhenAlreadyFiringIsNoOp() {
        var firing = openInstance(AlertState.FIRING, NOW.minusSeconds(120), null);
        var next = AlertStateTransitions.apply(firing, FIRING_RESULT, ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }

    @Test
    void firing_with_ack_stays_firing_on_next_firing_tick() {
        var current = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null)
                .withAck("alice", Instant.parse("2026-04-21T10:00:00Z"));
        var next = AlertStateTransitions.apply(
                current, new EvalResult.Firing(1.0, null, Map.of()), ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }

    // -------------------------------------------------------------------------
    // Batch + Error → always empty
    // -------------------------------------------------------------------------

    @Test
    void batchResultAlwaysEmpty() {
        var batch = new EvalResult.Batch(List.of(FIRING_RESULT), Map.of());
        var next = AlertStateTransitions.apply(null, batch, ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }

    @Test
    void errorResultAlwaysEmpty() {
        var next = AlertStateTransitions.apply(null,
                new EvalResult.Error(new RuntimeException("fail")), ruleWith(0), NOW);
        assertThat(next).isEmpty();
    }
}
