package com.cameleer.server.app.storage;

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.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.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;

/**
 * Verifies the ingest→store→read pipeline preserves processor-tree shape and
 * exchange bodies. All assertions go through the REST search + execution-
 * detail endpoints — the processor tree returned there is reconstructed by
 * DetailService.buildTree from the flat processor_executions rows, so it
 * exercises both the write path (flattening) and the read path (tree build).
 */
class IngestionSchemaIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestSecurityHelper securityHelper;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private final String agentId = "test-agent-ingestion-schema-it";
    private HttpHeaders authHeaders;
    private HttpHeaders viewerHeaders;

    @BeforeEach
    void setUp() {
        // 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));
        String jwt = securityHelper.registerTestAgent(agentId);
        authHeaders = securityHelper.authHeaders(jwt);
        viewerHeaders = securityHelper.authHeadersNoBody(securityHelper.viewerToken());
    }

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

    @Test
    void processorTreeMetadata_depthsAndParentIdsCorrect() throws Exception {
        String json = """
                {
                  "exchangeId": "ex-tree-1",
                  "applicationId": "test-group",
                  "instanceId": "%s",
                  "routeId": "schema-test-tree",
                  "correlationId": "corr-tree-1",
                  "status": "COMPLETED",
                  "startTime": "2026-03-11T10:00:00Z",
                  "endTime": "2026-03-11T10:00:01Z",
                  "durationMs": 1000,
                  "chunkSeq": 0,
                  "final": true,
                  "processors": [
                    {
                      "seq": 1,
                      "processorId": "root-proc",
                      "processorType": "bean",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00Z",
                      "durationMs": 500,
                      "inputBody": "root-input",
                      "outputBody": "root-output",
                      "inputHeaders": {"Content-Type": "application/json"},
                      "outputHeaders": {"X-Result": "ok"}
                    },
                    {
                      "seq": 2,
                      "parentSeq": 1,
                      "parentProcessorId": "root-proc",
                      "processorId": "child-proc",
                      "processorType": "log",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00.100Z",
                      "durationMs": 300,
                      "inputBody": "child-input",
                      "outputBody": "child-output"
                    },
                    {
                      "seq": 3,
                      "parentSeq": 2,
                      "parentProcessorId": "child-proc",
                      "processorId": "grandchild-proc",
                      "processorType": "setHeader",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00.200Z",
                      "durationMs": 100
                    }
                  ]
                }
                """.formatted(agentId);

        postExecution(json);

        JsonNode detail = awaitExecutionDetail("corr-tree-1");
        JsonNode processors = detail.get("processors");
        assertThat(processors).isNotNull();
        assertThat(processors).hasSize(1); // single root in the reconstructed tree

        JsonNode root = processors.get(0);
        assertThat(root.get("processorId").asText()).isEqualTo("root-proc");
        assertThat(root.get("processorType").asText()).isEqualTo("bean");
        assertThat(root.get("children")).hasSize(1);

        JsonNode child = root.get("children").get(0);
        assertThat(child.get("processorId").asText()).isEqualTo("child-proc");
        assertThat(child.get("children")).hasSize(1);

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

    @Test
    void exchangeBodiesStored() throws Exception {
        String json = """
                {
                  "exchangeId": "ex-bodies-1",
                  "applicationId": "test-group",
                  "instanceId": "%s",
                  "routeId": "schema-test-bodies",
                  "correlationId": "corr-bodies-1",
                  "status": "COMPLETED",
                  "startTime": "2026-03-11T10:00:00Z",
                  "endTime": "2026-03-11T10:00:01Z",
                  "durationMs": 1000,
                  "chunkSeq": 0,
                  "final": true,
                  "processors": [
                    {
                      "seq": 1,
                      "processorId": "proc-1",
                      "processorType": "bean",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00Z",
                      "durationMs": 500,
                      "inputBody": "processor-body-text",
                      "outputBody": "processor-output-text"
                    }
                  ]
                }
                """.formatted(agentId);

        postExecution(json);

        JsonNode detail = awaitExecutionDetail("corr-bodies-1");
        String execId = detail.get("executionId").asText();

        // Processor bodies are served via the detail processor-snapshot route
        // (see rules: GET /api/v1/executions/{id}/processors/{seq}/snapshot).
        ResponseEntity<String> snap = restTemplate.exchange(
                "/api/v1/executions/" + execId + "/processors/0/snapshot",
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class);
        assertThat(snap.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode snapBody = objectMapper.readTree(snap.getBody());
        assertThat(snapBody.get("inputBody").asText()).isEqualTo("processor-body-text");
        assertThat(snapBody.get("outputBody").asText()).isEqualTo("processor-output-text");
    }

    @Test
    void nullSnapshots_insertSucceedsWithEmptyDefaults() throws Exception {
        String json = """
                {
                  "exchangeId": "ex-null-1",
                  "applicationId": "test-group",
                  "instanceId": "%s",
                  "routeId": "schema-test-null-snap",
                  "correlationId": "corr-null-1",
                  "status": "COMPLETED",
                  "startTime": "2026-03-11T10:00:00Z",
                  "endTime": "2026-03-11T10:00:01Z",
                  "durationMs": 1000,
                  "chunkSeq": 0,
                  "final": true,
                  "processors": [
                    {
                      "seq": 1,
                      "processorId": "proc-null",
                      "processorType": "log",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00Z",
                      "durationMs": 500
                    }
                  ]
                }
                """.formatted(agentId);

        postExecution(json);

        JsonNode detail = awaitExecutionDetail("corr-null-1");
        JsonNode processors = detail.get("processors");
        assertThat(processors).isNotNull();
        assertThat(processors).hasSize(1);
        JsonNode root = processors.get(0);
        assertThat(root.get("processorId").asText()).isEqualTo("proc-null");
        // Root has no parent in the reconstructed tree.
        assertThat(root.get("children")).isEmpty();
    }

    /**
     * Poll the search + detail endpoints until the execution shows up, then
     * return the execution-detail JSON. Drives both CH writes and reads
     * through the full REST stack.
     */
    private JsonNode awaitExecutionDetail(String correlationId) throws Exception {
        JsonNode[] holder = new JsonNode[1];
        await().atMost(15, SECONDS).untilAsserted(() -> {
            ResponseEntity<String> search = restTemplate.exchange(
                    "/api/v1/environments/default/executions?correlationId=" + correlationId,
                    HttpMethod.GET,
                    new HttpEntity<>(viewerHeaders),
                    String.class);
            assertThat(search.getStatusCode()).isEqualTo(HttpStatus.OK);
            JsonNode body = objectMapper.readTree(search.getBody());
            assertThat(body.get("total").asLong()).isGreaterThanOrEqualTo(1);
            String execId = body.get("data").get(0).get("executionId").asText();

            ResponseEntity<String> detail = restTemplate.exchange(
                    "/api/v1/executions/" + execId,
                    HttpMethod.GET,
                    new HttpEntity<>(viewerHeaders),
                    String.class);
            assertThat(detail.getStatusCode()).isEqualTo(HttpStatus.OK);
            holder[0] = objectMapper.readTree(detail.getBody());
        });
        return holder[0];
    }

    private void postExecution(String json) {
        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/data/executions",
                new HttpEntity<>(json, authHeaders),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
    }
}
