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

import com.cameleer.server.app.search.ClickHouseLogStore;
import com.cameleer.server.core.alerting.*;
import com.cameleer.server.core.runtime.Environment;
import com.cameleer.server.core.runtime.EnvironmentRepository;
import com.cameleer.server.core.search.LogSearchRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class LogPatternEvaluatorTest {

    private ClickHouseLogStore logStore;
    private EnvironmentRepository envRepo;
    private LogPatternEvaluator eval;

    private static final UUID ENV_ID  = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
    private static final UUID RULE_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
    private static final Instant NOW  = Instant.parse("2026-04-19T10:00:00Z");

    @BeforeEach
    void setUp() {
        logStore = mock(ClickHouseLogStore.class);
        envRepo  = mock(EnvironmentRepository.class);
        eval = new LogPatternEvaluator(logStore, envRepo);

        var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, "slate", null);
        when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
    }

    private AlertRule ruleWith(AlertCondition condition) {
        return new AlertRule(RULE_ID, ENV_ID, "test", null,
                AlertSeverity.WARNING, true, condition.kind(), condition,
                60, 0, 0, null, null, List.of(), List.of(),
                null, null, null, Map.of(), null, null, null, null);
    }

    @Test
    void firesWhenCountExceedsThreshold() {
        var condition = new LogPatternCondition(
                new AlertScope("orders", null, null), "ERROR", "OutOfMemory", 5, 300);
        when(logStore.countLogs(any())).thenReturn(7L);

        EvalResult r = eval.evaluate(condition, ruleWith(condition), new EvalContext("default", NOW, new TickCache()));
        assertThat(r).isInstanceOf(EvalResult.Firing.class);
        var f = (EvalResult.Firing) r;
        assertThat(f.currentValue()).isEqualTo(7.0);
        assertThat(f.threshold()).isEqualTo(5.0);
    }

    @Test
    void clearWhenCountBelowThreshold() {
        var condition = new LogPatternCondition(
                new AlertScope("orders", null, null), "ERROR", "OutOfMemory", 5, 300);
        when(logStore.countLogs(any())).thenReturn(3L);

        EvalResult r = eval.evaluate(condition, ruleWith(condition), new EvalContext("default", NOW, new TickCache()));
        assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE);
    }

    @Test
    void clearWhenCountEqualsThreshold() {
        // threshold is GT (strictly greater), so equal should be Clear
        var condition = new LogPatternCondition(
                new AlertScope("orders", null, null), "ERROR", "OutOfMemory", 5, 300);
        when(logStore.countLogs(any())).thenReturn(5L);

        EvalResult r = eval.evaluate(condition, ruleWith(condition), new EvalContext("default", NOW, new TickCache()));
        assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE);
    }

    @Test
    void passesCorrectFieldsToLogStore() {
        var condition = new LogPatternCondition(
                new AlertScope("orders", null, null), "WARN", "timeout", 1, 120);
        when(logStore.countLogs(any())).thenReturn(2L);

        eval.evaluate(condition, ruleWith(condition), new EvalContext("default", NOW, new TickCache()));

        ArgumentCaptor<LogSearchRequest> captor = ArgumentCaptor.forClass(LogSearchRequest.class);
        verify(logStore).countLogs(captor.capture());
        LogSearchRequest req = captor.getValue();

        assertThat(req.application()).isEqualTo("orders");
        assertThat(req.levels()).contains("WARN");
        assertThat(req.q()).isEqualTo("timeout");
        assertThat(req.environment()).isEqualTo("prod");
        assertThat(req.from()).isEqualTo(NOW.minusSeconds(120));
        assertThat(req.to()).isEqualTo(NOW);
    }

    @Test
    void tickCacheCoalescesDuplicateQueries() {
        var condition = new LogPatternCondition(
                new AlertScope("orders", null, null), "ERROR", "NPE", 1, 300);
        when(logStore.countLogs(any())).thenReturn(2L);

        var cache = new TickCache();
        var ctx = new EvalContext("default", NOW, cache);
        var rule = ruleWith(condition);

        eval.evaluate(condition, rule, ctx);
        eval.evaluate(condition, rule, ctx); // same tick, same key

        // countLogs should only be called once due to TickCache coalescing
        verify(logStore, times(1)).countLogs(any());
    }

    @Test
    void kindIsLogPattern() {
        assertThat(eval.kind()).isEqualTo(ConditionKind.LOG_PATTERN);
    }
}
