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

import com.cameleer.server.app.alerting.config.AlertingProperties;
import com.cameleer.server.app.http.ApacheOutboundHttpClientFactory;
import com.cameleer.server.app.http.SslContextBuilder;
import com.cameleer.server.app.outbound.crypto.SecretCipher;
import com.cameleer.server.core.alerting.*;
import com.cameleer.server.core.http.OutboundHttpProperties;
import com.cameleer.server.core.http.TrustMode;
import com.cameleer.server.core.outbound.OutboundAuth;
import com.cameleer.server.core.outbound.OutboundConnection;
import com.cameleer.server.core.outbound.OutboundMethod;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * WireMock-backed integration tests for {@link WebhookDispatcher}.
 * Each test spins its own WireMock server (HTTP on random port, or HTTPS for TLS test).
 */
class WebhookDispatcherIT {

    private static final String JWT_SECRET = "very-secret-jwt-key-for-test-only-32chars";

    private WireMockServer wm;
    private WebhookDispatcher dispatcher;
    private SecretCipher cipher;

    @BeforeEach
    void setUp() {
        wm = new WireMockServer(WireMockConfiguration.options().dynamicPort());
        wm.start();

        OutboundHttpProperties props = new OutboundHttpProperties(
                false, List.of(), Duration.ofSeconds(2), Duration.ofSeconds(5), null, null, null);
        cipher = new SecretCipher(JWT_SECRET);
        dispatcher = new WebhookDispatcher(
                new ApacheOutboundHttpClientFactory(props, new SslContextBuilder()),
                cipher,
                new MustacheRenderer(),
                new AlertingProperties(null, null, null, null, null, null, null, null, null, null, null, null, null, null),
                new ObjectMapper()
        );
    }

    @AfterEach
    void tearDown() {
        if (wm != null) wm.stop();
    }

    // -------------------------------------------------------------------------
    // Tests
    // -------------------------------------------------------------------------

    @Test
    void twoHundredRespond_isDelivered() {
        wm.stubFor(post("/webhook").willReturn(aResponse().withStatus(200).withBody("accepted")));

        var outcome = dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.POST, null, Map.of(), null), ctx());

        assertThat(outcome.status()).isEqualTo(NotificationStatus.DELIVERED);
        assertThat(outcome.httpStatus()).isEqualTo(200);
        assertThat(outcome.snippet()).isEqualTo("accepted");
        assertThat(outcome.retryAfter()).isNull();
    }

    @Test
    void fourOhFour_isFailedImmediately() {
        wm.stubFor(post("/webhook").willReturn(aResponse().withStatus(404).withBody("not found")));

        var outcome = dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.POST, null, Map.of(), null), ctx());

        assertThat(outcome.status()).isEqualTo(NotificationStatus.FAILED);
        assertThat(outcome.httpStatus()).isEqualTo(404);
        assertThat(outcome.retryAfter()).isNull();
    }

    @Test
    void fiveOhThree_hasNullStatusAndRetryDelay() {
        wm.stubFor(post("/webhook").willReturn(aResponse().withStatus(503).withBody("unavailable")));

        var outcome = dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.POST, null, Map.of(), null), ctx());

        assertThat(outcome.status()).isNull();
        assertThat(outcome.httpStatus()).isEqualTo(503);
        assertThat(outcome.retryAfter()).isEqualTo(Duration.ofSeconds(30));
    }

    @Test
    void hmacHeader_presentWhenSecretSet() {
        wm.stubFor(post("/webhook").willReturn(ok("ok")));

        // Encrypt a test secret
        String ciphertext = cipher.encrypt("my-signing-secret");
        var outcome = dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.POST, ciphertext, Map.of(), null), ctx());

        assertThat(outcome.status()).isEqualTo(NotificationStatus.DELIVERED);
        wm.verify(postRequestedFor(urlEqualTo("/webhook"))
                .withHeader("X-Cameleer-Signature", matching("sha256=[0-9a-f]{64}")));
    }

    @Test
    void hmacHeader_absentWhenNoSecret() {
        wm.stubFor(post("/webhook").willReturn(ok("ok")));

        dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.POST, null, Map.of(), null), ctx());

        wm.verify(postRequestedFor(urlEqualTo("/webhook"))
                .withoutHeader("X-Cameleer-Signature"));
    }

    @Test
    void putMethod_isRespected() {
        wm.stubFor(put("/webhook").willReturn(ok("ok")));

        var outcome = dispatcher.dispatch(
                notif(null), null, instance(), conn(wm.port(), OutboundMethod.PUT, null, Map.of(), null), ctx());

        assertThat(outcome.status()).isEqualTo(NotificationStatus.DELIVERED);
        wm.verify(putRequestedFor(urlEqualTo("/webhook")));
    }

    @Test
    void customHeaderRenderedWithMustache() {
        wm.stubFor(post("/webhook").willReturn(ok("ok")));

        // "{{env.slug}}" in the defaultHeaders value should resolve to "dev" from context
        var headers = Map.of("X-Env", "{{env.slug}}");
        var outcome = dispatcher.dispatch(
                notif(null), null, instance(),
                conn(wm.port(), OutboundMethod.POST, null, headers, null),
                ctxWithEnv("dev"));

        assertThat(outcome.status()).isEqualTo(NotificationStatus.DELIVERED);
        wm.verify(postRequestedFor(urlEqualTo("/webhook"))
                .withHeader("X-Env", equalTo("dev")));
    }

    @Test
    void tlsTrustAll_worksAgainstSelfSignedCert() throws Exception {
        // Separate WireMock instance with HTTPS only
        WireMockServer wmHttps = new WireMockServer(
                WireMockConfiguration.options().httpDisabled(true).dynamicHttpsPort());
        wmHttps.start();
        wmHttps.stubFor(post("/webhook").willReturn(ok("secure-ok")));

        try {
            // Connection with TRUST_ALL so the self-signed cert is accepted
            var conn = connHttps(wmHttps.httpsPort(), OutboundMethod.POST, null, Map.of());
            var outcome = dispatcher.dispatch(notif(null), null, instance(), conn, ctx());
            assertThat(outcome.status()).isEqualTo(NotificationStatus.DELIVERED);
            assertThat(outcome.snippet()).isEqualTo("secure-ok");
        } finally {
            wmHttps.stop();
        }
    }

    // -------------------------------------------------------------------------
    // Builders
    // -------------------------------------------------------------------------

    private AlertNotification notif(UUID webhookId) {
        return new AlertNotification(
                UUID.randomUUID(), UUID.randomUUID(),
                webhookId, UUID.randomUUID(),
                NotificationStatus.PENDING, 0, Instant.now(),
                null, null, null, null, Map.of(), null, Instant.now());
    }

    private AlertInstance instance() {
        return new AlertInstance(
                UUID.randomUUID(), UUID.randomUUID(), Map.of(),
                UUID.randomUUID(), AlertState.FIRING, AlertSeverity.WARNING,
                Instant.now(), null, null, null, null, null, null, false,
                null, null, Map.of(), "Alert", "Message",
                List.of(), List.of(), List.of());
    }

    private OutboundConnection conn(int port, OutboundMethod method, String hmacCiphertext,
                                    Map<String, String> defaultHeaders, String bodyTmpl) {
        return new OutboundConnection(
                UUID.randomUUID(), "default", "test-conn", null,
                "http://localhost:" + port + "/webhook",
                method, defaultHeaders, bodyTmpl,
                TrustMode.SYSTEM_DEFAULT, List.of(),
                hmacCiphertext, new OutboundAuth.None(),
                List.of(), Instant.now(), "system", Instant.now(), "system");
    }

    private OutboundConnection connHttps(int port, OutboundMethod method, String hmacCiphertext,
                                          Map<String, String> defaultHeaders) {
        return new OutboundConnection(
                UUID.randomUUID(), "default", "test-conn-https", null,
                "https://localhost:" + port + "/webhook",
                method, defaultHeaders, null,
                TrustMode.TRUST_ALL, List.of(),
                hmacCiphertext, new OutboundAuth.None(),
                List.of(), Instant.now(), "system", Instant.now(), "system");
    }

    private Map<String, Object> ctx() {
        return Map.of(
                "env",   Map.of("slug", "prod", "id", UUID.randomUUID().toString()),
                "rule",  Map.of("name", "test-rule", "severity", "WARNING", "id", UUID.randomUUID().toString(), "description", ""),
                "alert", Map.of("id", UUID.randomUUID().toString(), "state", "FIRING", "firedAt", Instant.now().toString(),
                        "resolvedAt", "", "ackedBy", "", "link", "/alerts/inbox/x", "currentValue", "", "threshold", "")
        );
    }

    private Map<String, Object> ctxWithEnv(String envSlug) {
        return Map.of(
                "env",   Map.of("slug", envSlug, "id", UUID.randomUUID().toString()),
                "rule",  Map.of("name", "test-rule", "severity", "WARNING", "id", UUID.randomUUID().toString(), "description", ""),
                "alert", Map.of("id", UUID.randomUUID().toString(), "state", "FIRING", "firedAt", Instant.now().toString(),
                        "resolvedAt", "", "ackedBy", "", "link", "/alerts/inbox/x", "currentValue", "", "threshold", "")
        );
    }
}
