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.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
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.util.Map;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

/**
 * Integration tests for the detail and processor snapshot endpoints.
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DetailControllerIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestSecurityHelper securityHelper;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private String jwt;
    private String viewerJwt;
    private String seededExecutionId;

    /**
     * Seed a route execution with a 3-level processor tree:
     * root -> [child1, child2], child2 -> [grandchild]
     *
     * Uses the chunked ingestion envelope (POST /api/v1/data/executions →
     * ChunkIngestionController), which is the only active ingestion path.
     * The processor tree is flattened into FlatProcessorRecord[] with
     * seq / parentSeq; DetailService.buildTree reconstructs the nested
     * shape for the API response.
     */
    @BeforeAll
    void seedTestData() {
        // Lift max_agents cap so this IT (which registers an agent) isn't gated by license
        // enforcement. Cap behaviour itself is exercised by AgentCapEnforcementIT.
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100));
        jwt = securityHelper.registerTestAgent("test-agent-detail-it");
        viewerJwt = securityHelper.viewerToken();

        String json = """
                {
                  "exchangeId": "detail-ex-1",
                  "applicationId": "test-group",
                  "instanceId": "test-agent-detail-it",
                  "routeId": "detail-test-route",
                  "correlationId": "detail-corr-1",
                  "status": "COMPLETED",
                  "startTime": "2026-03-10T10:00:00Z",
                  "endTime": "2026-03-10T10:00:01Z",
                  "durationMs": 1000,
                  "chunkSeq": 0,
                  "final": true,
                  "processors": [
                    {
                      "seq": 1,
                      "processorId": "root-proc",
                      "processorType": "split",
                      "status": "COMPLETED",
                      "startTime": "2026-03-10T10:00:00Z",
                      "durationMs": 1000,
                      "inputBody": "root-input-body",
                      "outputBody": "root-output-body",
                      "inputHeaders": {"Content-Type": "application/json"},
                      "outputHeaders": {"X-Result": "ok"}
                    },
                    {
                      "seq": 2,
                      "parentSeq": 1,
                      "parentProcessorId": "root-proc",
                      "processorId": "child1-proc",
                      "processorType": "log",
                      "status": "COMPLETED",
                      "startTime": "2026-03-10T10:00:00.100Z",
                      "durationMs": 100,
                      "inputBody": "child1-input",
                      "outputBody": "child1-output"
                    },
                    {
                      "seq": 3,
                      "parentSeq": 1,
                      "parentProcessorId": "root-proc",
                      "processorId": "child2-proc",
                      "processorType": "bean",
                      "status": "COMPLETED",
                      "startTime": "2026-03-10T10:00:00.200Z",
                      "durationMs": 600,
                      "inputBody": "child2-input",
                      "outputBody": "child2-output"
                    },
                    {
                      "seq": 4,
                      "parentSeq": 3,
                      "parentProcessorId": "child2-proc",
                      "processorId": "grandchild-proc",
                      "processorType": "to",
                      "status": "COMPLETED",
                      "startTime": "2026-03-10T10:00:00.300Z",
                      "durationMs": 400,
                      "inputBody": "gc-input",
                      "outputBody": "gc-output",
                      "inputHeaders": {"X-GC": "true"}
                    }
                  ]
                }
                """;

        ingest(json);

        // Wait for async ingestion + flush, then pull the CH-assigned execution_id
        // back through the REST search API. Executions live in ClickHouse; always
        // drive CH assertions through REST so the test still covers the full
        // controller→service→store wiring.
        await().atMost(20, SECONDS).untilAsserted(() -> {
            ResponseEntity<String> r = restTemplate.exchange(
                    "/api/v1/environments/default/executions?correlationId=detail-corr-1",
                    HttpMethod.GET,
                    new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                    String.class);
            assertThat(r.getStatusCode()).isEqualTo(HttpStatus.OK);
            JsonNode body = objectMapper.readTree(r.getBody());
            assertThat(body.get("total").asLong()).isGreaterThanOrEqualTo(1);
            seededExecutionId = body.get("data").get(0).get("executionId").asText();
        });
    }

    @Test
    void getDetail_returnsNestedProcessorTree() throws Exception {
        ResponseEntity<String> response = detailGet("/" + seededExecutionId);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = objectMapper.readTree(response.getBody());
        assertThat(body.get("executionId").asText()).isEqualTo(seededExecutionId);
        assertThat(body.get("routeId").asText()).isEqualTo("detail-test-route");
        assertThat(body.get("status").asText()).isEqualTo("COMPLETED");
        assertThat(body.get("durationMs").asLong()).isEqualTo(1000);

        // Check nested tree: 1 root
        JsonNode processors = body.get("processors");
        assertThat(processors).hasSize(1);

        // Root has 2 children
        JsonNode root = processors.get(0);
        assertThat(root.get("processorId").asText()).isEqualTo("root-proc");
        assertThat(root.get("processorType").asText()).isEqualTo("split");
        assertThat(root.get("children")).hasSize(2);

        // Child1 has no children
        JsonNode child1 = root.get("children").get(0);
        assertThat(child1.get("processorId").asText()).isEqualTo("child1-proc");
        assertThat(child1.get("children")).isEmpty();

        // Child2 has 1 grandchild
        JsonNode child2 = root.get("children").get(1);
        assertThat(child2.get("processorId").asText()).isEqualTo("child2-proc");
        assertThat(child2.get("children")).hasSize(1);

        JsonNode grandchild = child2.get("children").get(0);
        assertThat(grandchild.get("processorId").asText()).isEqualTo("grandchild-proc");
        assertThat(grandchild.get("children")).isEmpty();
    }

    @Test
    void getDetail_includesDiagramContentHash() throws Exception {
        ResponseEntity<String> response = detailGet("/" + seededExecutionId);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = objectMapper.readTree(response.getBody());
        assertThat(body.has("diagramContentHash")).isTrue();
    }

    @Test
    void getDetail_nonexistentId_returns404() {
        ResponseEntity<String> response = detailGet("/nonexistent-execution-id");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    @Test
    void getProcessorSnapshot_returnsExchangeData() throws Exception {
        ResponseEntity<String> response = detailGet(
                "/" + seededExecutionId + "/processors/0/snapshot");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = objectMapper.readTree(response.getBody());
        assertThat(body.get("inputBody").asText()).isEqualTo("root-input-body");
        assertThat(body.get("outputBody").asText()).isEqualTo("root-output-body");
        assertThat(body.get("inputHeaders").asText()).contains("Content-Type");
        assertThat(body.get("outputHeaders").asText()).contains("X-Result");
    }

    @Test
    void getProcessorSnapshot_outOfBoundsIndex_returns404() {
        ResponseEntity<String> response = detailGet(
                "/" + seededExecutionId + "/processors/999/snapshot");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    @Test
    void getProcessorSnapshot_nonexistentExecution_returns404() {
        ResponseEntity<String> response = detailGet(
                "/nonexistent-id/processors/0/snapshot");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    // --- Helper methods ---

    private void ingest(String json) {
        restTemplate.postForEntity("/api/v1/data/executions",
                new HttpEntity<>(json, securityHelper.authHeaders(jwt)), String.class);
    }

    private ResponseEntity<String> detailGet(String path) {
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        return restTemplate.exchange(
                "/api/v1/executions" + path,
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class);
    }

    @AfterAll
    void tearDown() {
        securityHelper.clearTestLicense();
    }
}
