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

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.app.search.ClickHouseLogStore;
import com.cameleer.server.core.admin.AuditRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.util.UUID;

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

class AlertRuleControllerIT extends AbstractPostgresIT {

    // ExchangeMatchEvaluator and LogPatternEvaluator depend on these concrete beans
    // (not the SearchIndex/LogIndex interfaces). Mock them so the context wires up.
    @MockBean(name = "clickHouseLogStore")    ClickHouseLogStore    clickHouseLogStore;

    @Autowired private TestRestTemplate restTemplate;
    @Autowired private ObjectMapper objectMapper;
    @Autowired private TestSecurityHelper securityHelper;
    @Autowired private AuditRepository auditRepository;

    private String operatorJwt;
    private String viewerJwt;
    private String envSlug;
    private UUID envId;

    @BeforeEach
    void setUp() {
        operatorJwt = securityHelper.operatorToken();
        viewerJwt   = securityHelper.viewerToken();
        seedUser("test-operator");
        seedUser("test-viewer");

        // Create a test environment
        envSlug = "test-env-" + UUID.randomUUID().toString().substring(0, 8);
        envId = UUID.randomUUID();
        jdbcTemplate.update(
                "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING",
                envId, envSlug, envSlug);
    }

    @AfterEach
    void cleanUp() {
        jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", envId);
        jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
        jdbcTemplate.update("DELETE FROM users WHERE user_id IN ('test-operator','test-viewer')");
    }

    // --- Happy path: POST creates rule, returns 201 ---

    @Test
    void operatorCanCreateRule() throws Exception {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("test-rule"), securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        JsonNode body = objectMapper.readTree(resp.getBody());
        assertThat(body.path("name").asText()).isEqualTo("test-rule");
        assertThat(body.path("id").asText()).isNotBlank();
        assertThat(body.path("enabled").asBoolean()).isTrue();
        assertThat(body.path("severity").asText()).isEqualTo("WARNING");
    }

    @Test
    void operatorCanListRules() {
        // Create a rule first
        restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("list-test"), securityHelper.authHeaders(operatorJwt)),
                String.class);

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    @Test
    void viewerCanList() {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    @Test
    void viewerCannotCreate() {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("viewer-rule"), securityHelper.authHeaders(viewerJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    }

    // --- Webhook validation ---

    @Test
    void unknownOutboundConnectionIdReturns422() {
        String body = """
                {"name":"bad-webhook","severity":"WARNING","conditionKind":"ROUTE_METRIC",
                 "condition":{"kind":"ROUTE_METRIC","scope":{},
                   "metric":"ERROR_RATE","comparator":"GT","threshold":0.05,"windowSeconds":60},
                 "webhooks":[{"outboundConnectionId":"%s"}]}
                """.formatted(UUID.randomUUID());

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
    }

    // --- Attribute key SQL injection prevention ---

    @Test
    void attributeKeyWithSqlMetaReturns422() {
        String body = """
                {"name":"sqli-test","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
                 "condition":{"kind":"EXCHANGE_MATCH","scope":{},
                   "filter":{"status":"FAILED","attributes":{"foo'; DROP TABLE executions; --":"x"}},
                   "fireMode":"PER_EXCHANGE"}}
                """;

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
        assertThat(resp.getBody()).contains("Invalid attribute key");
    }

    @Test
    void validAttributeKeyIsAccepted() throws Exception {
        String body = """
                {"name":"valid-attr","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
                 "condition":{"kind":"EXCHANGE_MATCH","scope":{},
                   "filter":{"status":"FAILED","attributes":{"order.type":"x"}},
                   "fireMode":"PER_EXCHANGE"},
                 "targets":[{"kind":"USER","targetId":"test-operator"}]}
                """;

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }

    // --- Enable / Disable ---

    @Test
    void enableAndDisable() throws Exception {
        ResponseEntity<String> create = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("toggle-rule"), securityHelper.authHeaders(operatorJwt)),
                String.class);
        String id = objectMapper.readTree(create.getBody()).path("id").asText();

        ResponseEntity<String> disabled = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules/" + id + "/disable",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(disabled.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(objectMapper.readTree(disabled.getBody()).path("enabled").asBoolean()).isFalse();

        ResponseEntity<String> enabled = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules/" + id + "/enable",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(enabled.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(objectMapper.readTree(enabled.getBody()).path("enabled").asBoolean()).isTrue();
    }

    // --- Delete emits audit event ---

    @Test
    void deleteEmitsAuditEvent() throws Exception {
        ResponseEntity<String> create = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("audit-rule"), securityHelper.authHeaders(operatorJwt)),
                String.class);
        String id = objectMapper.readTree(create.getBody()).path("id").asText();

        restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules/" + id,
                HttpMethod.DELETE,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);

        int count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM audit_log WHERE action = 'ALERT_RULE_DELETE' AND target = ?",
                Integer.class, id);
        assertThat(count).isGreaterThanOrEqualTo(1);
    }

    // --- Render preview ---

    @Test
    void renderPreview() throws Exception {
        ResponseEntity<String> create = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(routeMetricRuleBody("preview-rule"), securityHelper.authHeaders(operatorJwt)),
                String.class);
        String id = objectMapper.readTree(create.getBody()).path("id").asText();

        ResponseEntity<String> preview = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules/" + id + "/render-preview",
                HttpMethod.POST,
                new HttpEntity<>("{\"context\":{}}", securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(preview.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    // --- PER_EXCHANGE cross-field validation + empty-targets validation ---
    // RED tests: today's controller accepts these bodies; Task 3.3 adds the validator.

    @Test
    void createPerExchangeRule_withReNotifyMinutesNonZero_returns400() {
        String body = perExchangeRuleBodyWithExtras(
                "per-exchange-renotify",
                /*reNotifyMinutes*/ 60,
                /*forDurationSeconds*/ null);

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(resp.getBody()).contains("reNotifyMinutes");
    }

    @Test
    void createPerExchangeRule_withForDurationSecondsNonZero_returns400() {
        String body = perExchangeRuleBodyWithExtras(
                "per-exchange-forduration",
                /*reNotifyMinutes*/ null,
                /*forDurationSeconds*/ 60);

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(resp.getBody()).contains("forDurationSeconds");
    }

    @Test
    void createAnyRule_withEmptyWebhooksAndTargets_returns400() {
        // baseValidPerExchangeRuleRequest() already produces no webhooks / no targets — that's
        // precisely the "empty webhooks + empty targets" shape this test pins as a 400.
        String body = baseValidPerExchangeRuleRequest("no-sinks");

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/alerts/rules",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
        assertThat(resp.getBody()).satisfiesAnyOf(
                s -> assertThat(s).contains("webhook"),
                s -> assertThat(s).contains("target"));
    }

    // --- Unknown env returns 404 ---

    @Test
    void unknownEnvReturns404() {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/nonexistent-env-slug/alerts/rules",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

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

    private void seedUser(String userId) {
        jdbcTemplate.update(
                "INSERT INTO users (user_id, provider, email, display_name) VALUES (?, 'test', ?, ?) ON CONFLICT (user_id) DO NOTHING",
                userId, userId + "@example.com", userId);
    }

    private static String routeMetricRuleBody(String name) {
        // Includes a USER target so the rule passes the "at least one webhook or target" guard.
        return """
                {"name":"%s","severity":"WARNING","conditionKind":"ROUTE_METRIC",
                 "condition":{"kind":"ROUTE_METRIC","scope":{},
                   "metric":"ERROR_RATE","comparator":"GT","threshold":0.05,"windowSeconds":60},
                 "targets":[{"kind":"USER","targetId":"test-operator"}]}
                """.formatted(name);
    }

    /**
     * Produces a request body for a valid PER_EXCHANGE rule (baseline) — no webhooks,
     * no targets, no reNotifyMinutes, no forDurationSeconds. The controller currently
     * accepts this shape; Task 3.3 tightens that (empty sinks will 400).
     */
    private static String baseValidPerExchangeRuleRequest(String name) {
        return """
                {"name":"%s","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
                 "condition":{"kind":"EXCHANGE_MATCH","scope":{},
                   "filter":{"status":"FAILED","attributes":{}},
                   "fireMode":"PER_EXCHANGE"}}
                """.formatted(name);
    }

    /**
     * Variant of {@link #baseValidPerExchangeRuleRequest(String)} that sets
     * reNotifyMinutes and/or forDurationSeconds at the top-level request. Used to pin
     * the PER_EXCHANGE cross-field validation contract (Task 3.3).
     */
    private static String perExchangeRuleBodyWithExtras(String name,
                                                        Integer reNotifyMinutes,
                                                        Integer forDurationSeconds) {
        StringBuilder extras = new StringBuilder();
        if (reNotifyMinutes != null) {
            extras.append(",\"reNotifyMinutes\":").append(reNotifyMinutes);
        }
        if (forDurationSeconds != null) {
            extras.append(",\"forDurationSeconds\":").append(forDurationSeconds);
        }
        return """
                {"name":"%s","severity":"WARNING","conditionKind":"EXCHANGE_MATCH",
                 "condition":{"kind":"EXCHANGE_MATCH","scope":{},
                   "filter":{"status":"FAILED","attributes":{}},
                   "fireMode":"PER_EXCHANGE"}%s}
                """.formatted(name, extras.toString());
    }
}
