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

import com.cameleer.server.core.alerting.*;
import com.cameleer.server.core.runtime.Environment;
import org.junit.jupiter.api.BeforeEach;
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 NotificationContextBuilderTest {

    private NotificationContextBuilder builder;

    private static final UUID ENV_ID  = UUID.fromString("11111111-1111-1111-1111-111111111111");
    private static final UUID RULE_ID = UUID.fromString("22222222-2222-2222-2222-222222222222");
    private static final UUID INST_ID = UUID.fromString("33333333-3333-3333-3333-333333333333");

    @BeforeEach
    void setUp() {
        builder = new NotificationContextBuilder();
    }

    // ---- helpers ----

    private Environment env() {
        return new Environment(ENV_ID, "prod", "Production", true, true, Map.of(), 5, "slate", Instant.EPOCH);
    }

    private AlertRule rule(ConditionKind kind) {
        AlertCondition condition = switch (kind) {
            case ROUTE_METRIC     -> new RouteMetricCondition(
                                         new AlertScope("my-app", "route-1", null),
                                         RouteMetric.ERROR_RATE, Comparator.GT, 0.1, 60);
            case EXCHANGE_MATCH   -> new ExchangeMatchCondition(
                                         new AlertScope("my-app", "route-1", null),
                                         new ExchangeMatchCondition.ExchangeFilter("FAILED", Map.of()),
                                         FireMode.PER_EXCHANGE, null, null);
            case AGENT_STATE      -> new AgentStateCondition(
                                         new AlertScope(null, null, null),
                                         "DEAD", 0);
            case AGENT_LIFECYCLE  -> new AgentLifecycleCondition(
                                         new AlertScope(null, null, null),
                                         List.of(AgentLifecycleEventType.WENT_DEAD),
                                         60);
            case DEPLOYMENT_STATE -> new DeploymentStateCondition(
                                         new AlertScope("my-app", null, null),
                                         List.of("FAILED"));
            case LOG_PATTERN      -> new LogPatternCondition(
                                         new AlertScope("my-app", null, null),
                                         "ERROR", "OutOfMemory", 5, 60);
            case JVM_METRIC       -> new JvmMetricCondition(
                                         new AlertScope(null, null, "agent-1"),
                                         "heap.used", AggregationOp.MAX, Comparator.GT, 90.0, 300);
        };
        return new AlertRule(
                RULE_ID, ENV_ID, "High error rate", "Alert description",
                AlertSeverity.CRITICAL, true,
                kind, condition,
                60, 120, 30,
                "{{rule.name}} fired", "Value: {{alert.currentValue}}",
                List.of(), List.of(),
                Instant.now(), null, Instant.now(),
                Map.of(),
                Instant.now(), "admin", Instant.now(), "admin"
        );
    }

    private AlertInstance instance(Map<String, Object> ctx) {
        return new AlertInstance(
                INST_ID, RULE_ID, Map.of(), ENV_ID,
                AlertState.FIRING, AlertSeverity.CRITICAL,
                Instant.parse("2026-04-19T10:00:00Z"),
                null, null, null, null, null, null,
                false, 0.95, 0.1,
                ctx, "Alert fired", "Some message",
                List.of(), List.of(), List.of()
        );
    }

    // ---- env / rule / alert subtrees always present ----

    @Test
    void envSubtree_alwaysPresent() {
        var inst = instance(Map.of("route", Map.of("id", "route-1"), "app", Map.of("slug", "my-app")));
        var ctx  = builder.build(rule(ConditionKind.ROUTE_METRIC), inst, env(), null);

        assertThat(ctx).containsKey("env");
        @SuppressWarnings("unchecked") var env = (Map<String, Object>) ctx.get("env");
        assertThat(env).containsEntry("slug", "prod")
                       .containsEntry("id", ENV_ID.toString());
    }

    @Test
    void ruleSubtree_alwaysPresent() {
        var inst = instance(Map.of());
        var ctx  = builder.build(rule(ConditionKind.AGENT_STATE), inst, env(), null);

        @SuppressWarnings("unchecked") var ruleMap = (Map<String, Object>) ctx.get("rule");
        assertThat(ruleMap).containsEntry("id", RULE_ID.toString())
                           .containsEntry("name", "High error rate")
                           .containsEntry("severity", "CRITICAL")
                           .containsEntry("description", "Alert description");
    }

    @Test
    void alertSubtree_alwaysPresent() {
        var inst = instance(Map.of());
        var ctx  = builder.build(rule(ConditionKind.AGENT_STATE), inst, env(), "https://ui.example.com");

        @SuppressWarnings("unchecked") var alert = (Map<String, Object>) ctx.get("alert");
        assertThat(alert).containsEntry("id", INST_ID.toString())
                         .containsEntry("state", "FIRING")
                         .containsEntry("firedAt", "2026-04-19T10:00:00Z")
                         .containsEntry("currentValue", "0.95")
                         .containsEntry("threshold", "0.1");
    }

    @Test
    void alertLink_withUiOrigin() {
        var inst = instance(Map.of());
        var ctx  = builder.build(rule(ConditionKind.AGENT_STATE), inst, env(), "https://ui.example.com");

        @SuppressWarnings("unchecked") var alert = (Map<String, Object>) ctx.get("alert");
        assertThat(alert.get("link")).isEqualTo("https://ui.example.com/alerts/inbox/" + INST_ID);
    }

    @Test
    void alertLink_withoutUiOrigin_isRelative() {
        var inst = instance(Map.of());
        var ctx  = builder.build(rule(ConditionKind.AGENT_STATE), inst, env(), null);

        @SuppressWarnings("unchecked") var alert = (Map<String, Object>) ctx.get("alert");
        assertThat(alert.get("link")).isEqualTo("/alerts/inbox/" + INST_ID);
    }

    // ---- conditional subtrees by kind ----

    @Test
    void exchangeMatch_hasExchangeAppRoute_butNotLogOrMetric() {
        var ctx = Map.<String, Object>of(
                "exchange", Map.of("id", "ex-99", "status", "FAILED"),
                "app",      Map.of("slug", "my-app", "id", "app-uuid"),
                "route",    Map.of("id", "route-1", "uri", "direct:start"));
        var result = builder.build(rule(ConditionKind.EXCHANGE_MATCH), instance(ctx), env(), null);

        assertThat(result).containsKeys("exchange", "app", "route")
                          .doesNotContainKey("log")
                          .doesNotContainKey("metric")
                          .doesNotContainKey("agent");
    }

    @Test
    void agentState_hasAgentAndApp_butNotRoute() {
        var ctx = Map.<String, Object>of(
                "agent", Map.of("id", "a-42", "name", "my-agent", "state", "DEAD"),
                "app",   Map.of("slug", "my-app", "id", "app-uuid"));
        var result = builder.build(rule(ConditionKind.AGENT_STATE), instance(ctx), env(), null);

        assertThat(result).containsKeys("agent", "app")
                          .doesNotContainKey("route")
                          .doesNotContainKey("exchange")
                          .doesNotContainKey("log")
                          .doesNotContainKey("metric");
    }

    @Test
    void routeMetric_hasRouteAndApp_butNotAgentOrExchange() {
        var ctx = Map.<String, Object>of(
                "route", Map.of("id", "route-1", "uri", "timer://tick"),
                "app",   Map.of("slug", "my-app", "id", "app-uuid"));
        var result = builder.build(rule(ConditionKind.ROUTE_METRIC), instance(ctx), env(), null);

        assertThat(result).containsKeys("route", "app")
                          .doesNotContainKey("agent")
                          .doesNotContainKey("exchange")
                          .doesNotContainKey("log");
    }

    @Test
    void logPattern_hasLogAndApp_butNotRouteOrAgent() {
        var ctx = Map.<String, Object>of(
                "log", Map.of("pattern", "ERROR", "matchCount", "7"),
                "app", Map.of("slug", "my-app", "id", "app-uuid"));
        var result = builder.build(rule(ConditionKind.LOG_PATTERN), instance(ctx), env(), null);

        assertThat(result).containsKeys("log", "app")
                          .doesNotContainKey("route")
                          .doesNotContainKey("agent")
                          .doesNotContainKey("metric");
    }

    @Test
    void jvmMetric_hasMetricAgentAndApp() {
        var ctx = Map.<String, Object>of(
                "metric", Map.of("name", "heap.used", "value", "88.5"),
                "agent",  Map.of("id", "a-42", "name", "my-agent"),
                "app",    Map.of("slug", "my-app", "id", "app-uuid"));
        var result = builder.build(rule(ConditionKind.JVM_METRIC), instance(ctx), env(), null);

        assertThat(result).containsKeys("metric", "agent", "app")
                          .doesNotContainKey("route")
                          .doesNotContainKey("exchange")
                          .doesNotContainKey("log");
    }

    @Test
    void missingContextValues_emitEmptyString() {
        // Empty context — subtree values should all be empty string, not null.
        var inst = instance(Map.of());
        var result = builder.build(rule(ConditionKind.ROUTE_METRIC), inst, env(), null);

        @SuppressWarnings("unchecked") var route = (Map<String, Object>) result.get("route");
        assertThat(route.get("id")).isEqualTo("");
        assertThat(route.get("uri")).isEqualTo("");
    }
}
