package com.cameleer.server.app.controller;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.sql.Timestamp;
import java.time.Instant;
import java.util.Map;

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

class ServerMetricsAdminControllerIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestSecurityHelper securityHelper;

    private final ObjectMapper mapper = new ObjectMapper();

    private HttpHeaders adminJson;
    private HttpHeaders adminGet;
    private HttpHeaders viewerGet;

    @BeforeEach
    void seedAndAuth() {
        adminJson = securityHelper.adminHeaders();
        adminGet  = securityHelper.authHeadersNoBody(securityHelper.adminToken());
        viewerGet = securityHelper.authHeadersNoBody(securityHelper.viewerToken());

        // Fresh rows for each test. The Spring-context ClickHouse JdbcTemplate
        // lives in a different bean; reach for it here by executing through
        // the same JdbcTemplate used by the store via the ClickHouseConfig bean.
        org.springframework.jdbc.core.JdbcTemplate ch = clickhouseJdbc();
        ch.execute("TRUNCATE TABLE server_metrics");

        Instant t0 = Instant.parse("2026-04-23T10:00:00Z");
        // Gauge: cameleer.agents.connected, two states, two buckets.
        insert(ch, "default", t0, "srv-A", "cameleer.agents.connected", "gauge", "value", 3.0,
                Map.of("state", "live"));
        insert(ch, "default", t0.plusSeconds(60), "srv-A", "cameleer.agents.connected", "gauge", "value", 4.0,
                Map.of("state", "live"));
        insert(ch, "default", t0, "srv-A", "cameleer.agents.connected", "gauge", "value", 1.0,
                Map.of("state", "stale"));
        insert(ch, "default", t0.plusSeconds(60), "srv-A", "cameleer.agents.connected", "gauge", "value", 0.0,
                Map.of("state", "stale"));

        // Counter: cumulative drops, +5 per minute on srv-A.
        insert(ch, "default", t0,                  "srv-A", "cameleer.ingestion.drops", "counter", "count",  0.0, Map.of("reason", "buffer_full"));
        insert(ch, "default", t0.plusSeconds(60),  "srv-A", "cameleer.ingestion.drops", "counter", "count",  5.0, Map.of("reason", "buffer_full"));
        insert(ch, "default", t0.plusSeconds(120), "srv-A", "cameleer.ingestion.drops", "counter", "count", 10.0, Map.of("reason", "buffer_full"));
        // Simulated restart to srv-B: counter resets to 0, then climbs to 2.
        insert(ch, "default", t0.plusSeconds(180), "srv-B", "cameleer.ingestion.drops", "counter", "count",  0.0, Map.of("reason", "buffer_full"));
        insert(ch, "default", t0.plusSeconds(240), "srv-B", "cameleer.ingestion.drops", "counter", "count",  2.0, Map.of("reason", "buffer_full"));

        // Timer mean inputs: two buckets, 2 samples each (count=2, total_time=30).
        insert(ch, "default", t0,                 "srv-A", "cameleer.ingestion.flush.duration", "timer", "count",      2.0, Map.of("type", "execution"));
        insert(ch, "default", t0,                 "srv-A", "cameleer.ingestion.flush.duration", "timer", "total_time", 30.0, Map.of("type", "execution"));
        insert(ch, "default", t0.plusSeconds(60), "srv-A", "cameleer.ingestion.flush.duration", "timer", "count",      4.0, Map.of("type", "execution"));
        insert(ch, "default", t0.plusSeconds(60), "srv-A", "cameleer.ingestion.flush.duration", "timer", "total_time", 100.0, Map.of("type", "execution"));
    }

    // ── catalog ─────────────────────────────────────────────────────────

    @Test
    void catalog_listsSeededMetricsWithStatisticsAndTagKeys() throws Exception {
        ResponseEntity<String> r = restTemplate.exchange(
                "/api/v1/admin/server-metrics/catalog?from=2026-04-23T09:00:00Z&to=2026-04-23T11:00:00Z",
                HttpMethod.GET, new HttpEntity<>(adminGet), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = mapper.readTree(r.getBody());
        assertThat(body.isArray()).isTrue();

        JsonNode drops = findByField(body, "metricName", "cameleer.ingestion.drops");
        assertThat(drops.get("metricType").asText()).isEqualTo("counter");
        assertThat(asStringList(drops.get("statistics"))).contains("count");
        assertThat(asStringList(drops.get("tagKeys"))).contains("reason");

        JsonNode timer = findByField(body, "metricName", "cameleer.ingestion.flush.duration");
        assertThat(asStringList(timer.get("statistics"))).contains("count", "total_time");
    }

    // ── instances ───────────────────────────────────────────────────────

    @Test
    void instances_listsDistinctServerInstanceIdsWithFirstAndLastSeen() throws Exception {
        ResponseEntity<String> r = restTemplate.exchange(
                "/api/v1/admin/server-metrics/instances?from=2026-04-23T09:00:00Z&to=2026-04-23T11:00:00Z",
                HttpMethod.GET, new HttpEntity<>(adminGet), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = mapper.readTree(r.getBody());
        assertThat(body.isArray()).isTrue();
        assertThat(body.size()).isEqualTo(2);
        // Ordered by last_seen DESC — srv-B saw a later row.
        assertThat(body.get(0).get("serverInstanceId").asText()).isEqualTo("srv-B");
        assertThat(body.get(1).get("serverInstanceId").asText()).isEqualTo("srv-A");
    }

    // ── query — gauge with group-by-tag ─────────────────────────────────

    @Test
    void query_gaugeWithGroupByTag_returnsSeriesPerTagValue() throws Exception {
        String requestBody = """
                {
                  "metric": "cameleer.agents.connected",
                  "statistic": "value",
                  "from": "2026-04-23T09:59:00Z",
                  "to":   "2026-04-23T10:02:00Z",
                  "stepSeconds": 60,
                  "groupByTags": ["state"],
                  "aggregation": "avg",
                  "mode": "raw"
                }
                """;

        ResponseEntity<String> r = restTemplate.postForEntity(
                "/api/v1/admin/server-metrics/query",
                new HttpEntity<>(requestBody, adminJson), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = mapper.readTree(r.getBody());
        assertThat(body.get("metric").asText()).isEqualTo("cameleer.agents.connected");
        assertThat(body.get("statistic").asText()).isEqualTo("value");
        assertThat(body.get("mode").asText()).isEqualTo("raw");
        assertThat(body.get("stepSeconds").asInt()).isEqualTo(60);

        JsonNode series = body.get("series");
        assertThat(series.isArray()).isTrue();
        assertThat(series.size()).isEqualTo(2);

        JsonNode live = findByTag(series, "state", "live");
        assertThat(live.get("points").size()).isEqualTo(2);
        assertThat(live.get("points").get(0).get("v").asDouble()).isEqualTo(3.0);
        assertThat(live.get("points").get(1).get("v").asDouble()).isEqualTo(4.0);
    }

    // ── query — counter delta across instance rotation ──────────────────

    @Test
    void query_counterDelta_clipsNegativesAcrossInstanceRotation() throws Exception {
        String requestBody = """
                {
                  "metric": "cameleer.ingestion.drops",
                  "statistic": "count",
                  "from": "2026-04-23T09:59:00Z",
                  "to":   "2026-04-23T10:05:00Z",
                  "stepSeconds": 60,
                  "groupByTags": ["reason"],
                  "aggregation": "sum",
                  "mode": "delta"
                }
                """;

        ResponseEntity<String> r = restTemplate.postForEntity(
                "/api/v1/admin/server-metrics/query",
                new HttpEntity<>(requestBody, adminJson), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = mapper.readTree(r.getBody());
        JsonNode reason = findByTag(body.get("series"), "reason", "buffer_full");
        // Deltas: 0 (first bucket on srv-A), 5, 5, 0 (first on srv-B, clipped), 2.
        // Sum across the window should be 12 if we tally all positive deltas.
        double sum = 0;
        for (JsonNode p : reason.get("points")) sum += p.get("v").asDouble();
        assertThat(sum).isEqualTo(12.0);
        // No individual point may be negative.
        for (JsonNode p : reason.get("points")) {
            assertThat(p.get("v").asDouble()).isGreaterThanOrEqualTo(0.0);
        }
    }

    // ── query — derived 'mean' statistic for timers ─────────────────────

    @Test
    void query_timerMeanStatistic_computesTotalOverCountPerBucket() throws Exception {
        String requestBody = """
                {
                  "metric": "cameleer.ingestion.flush.duration",
                  "statistic": "mean",
                  "from": "2026-04-23T09:59:00Z",
                  "to":   "2026-04-23T10:02:00Z",
                  "stepSeconds": 60,
                  "groupByTags": ["type"],
                  "aggregation": "avg",
                  "mode": "raw"
                }
                """;

        ResponseEntity<String> r = restTemplate.postForEntity(
                "/api/v1/admin/server-metrics/query",
                new HttpEntity<>(requestBody, adminJson), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = mapper.readTree(r.getBody());
        JsonNode points = findByTag(body.get("series"), "type", "execution").get("points");
        // Bucket 0: 30 / 2 = 15.0
        // Bucket 1: 100 / 4 = 25.0
        assertThat(points.get(0).get("v").asDouble()).isEqualTo(15.0);
        assertThat(points.get(1).get("v").asDouble()).isEqualTo(25.0);
    }

    // ── query — input validation ────────────────────────────────────────

    @Test
    void query_rejectsUnsafeMetricName() {
        String requestBody = """
                {
                  "metric": "cameleer.agents; DROP TABLE server_metrics",
                  "from": "2026-04-23T09:59:00Z",
                  "to":   "2026-04-23T10:02:00Z"
                }
                """;

        ResponseEntity<String> r = restTemplate.postForEntity(
                "/api/v1/admin/server-metrics/query",
                new HttpEntity<>(requestBody, adminJson), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }

    @Test
    void query_rejectsRangeBeyondMax() {
        String requestBody = """
                {
                  "metric": "cameleer.agents.connected",
                  "from": "2026-01-01T00:00:00Z",
                  "to":   "2026-04-23T00:00:00Z"
                }
                """;

        ResponseEntity<String> r = restTemplate.postForEntity(
                "/api/v1/admin/server-metrics/query",
                new HttpEntity<>(requestBody, adminJson), String.class);
        assertThat(r.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }

    // ── authorization ───────────────────────────────────────────────────

    @Test
    void allEndpoints_requireAdminRole() {
        ResponseEntity<String> catalog = restTemplate.exchange(
                "/api/v1/admin/server-metrics/catalog",
                HttpMethod.GET, new HttpEntity<>(viewerGet), String.class);
        assertThat(catalog.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);

        ResponseEntity<String> instances = restTemplate.exchange(
                "/api/v1/admin/server-metrics/instances",
                HttpMethod.GET, new HttpEntity<>(viewerGet), String.class);
        assertThat(instances.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);

        HttpHeaders viewerPost = securityHelper.authHeaders(securityHelper.viewerToken());
        ResponseEntity<String> query = restTemplate.exchange(
                "/api/v1/admin/server-metrics/query",
                HttpMethod.POST, new HttpEntity<>("{}", viewerPost), String.class);
        assertThat(query.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    }

    // ── helpers ─────────────────────────────────────────────────────────

    private org.springframework.jdbc.core.JdbcTemplate clickhouseJdbc() {
        return org.springframework.test.util.AopTestUtils.getTargetObject(
                applicationContext.getBean("clickHouseJdbcTemplate"));
    }

    @Autowired
    private org.springframework.context.ApplicationContext applicationContext;

    private static void insert(org.springframework.jdbc.core.JdbcTemplate jdbc,
                               String tenantId, Instant collectedAt, String serverInstanceId,
                               String metricName, String metricType, String statistic,
                               double value, Map<String, String> tags) {
        jdbc.update("""
                INSERT INTO server_metrics
                    (tenant_id, collected_at, server_instance_id,
                     metric_name, metric_type, statistic, metric_value, tags)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                tenantId, Timestamp.from(collectedAt), serverInstanceId,
                metricName, metricType, statistic, value, tags);
    }

    private static JsonNode findByField(JsonNode array, String field, String value) {
        for (JsonNode n : array) {
            if (value.equals(n.path(field).asText())) return n;
        }
        throw new AssertionError("no element with " + field + "=" + value);
    }

    private static JsonNode findByTag(JsonNode seriesArray, String tagKey, String tagValue) {
        for (JsonNode s : seriesArray) {
            if (tagValue.equals(s.path("tags").path(tagKey).asText())) return s;
        }
        throw new AssertionError("no series with tag " + tagKey + "=" + tagValue);
    }

    private static java.util.List<String> asStringList(JsonNode arr) {
        java.util.List<String> out = new java.util.ArrayList<>();
        if (arr != null) for (JsonNode n : arr) out.add(n.asText());
        return out;
    }
}
