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;

/**
 * Integration test proving that diagram_content_hash is populated on
 * executions when a RouteGraph exists for the same route+agent. All
 * assertions go through the REST search + execution-detail endpoints
 * (no raw SQL against ClickHouse).
 */
class DiagramLinkingIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestSecurityHelper securityHelper;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private HttpHeaders authHeaders;
    private HttpHeaders viewerHeaders;
    private final String agentId = "test-agent-diagram-linking-it";

    @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 diagramHashPopulated_whenRouteGraphExistsBeforeExecution() throws Exception {
        String graphJson = """
                {
                  "routeId": "diagram-link-route",
                  "description": "Linking test diagram",
                  "version": 1,
                  "nodes": [
                    {"id": "n1", "type": "ENDPOINT", "label": "direct:start"},
                    {"id": "n2", "type": "BEAN", "label": "myBean"}
                  ],
                  "edges": [
                    {"source": "n1", "target": "n2", "edgeType": "FLOW"}
                  ]
                }
                """;

        ResponseEntity<String> diagramResponse = restTemplate.postForEntity(
                "/api/v1/data/diagrams",
                new HttpEntity<>(graphJson, authHeaders),
                String.class);
        assertThat(diagramResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);

        // Confirm the diagram is addressable via REST before we ingest the
        // execution — otherwise the ingestion-service hash lookup could miss
        // the not-yet-flushed graph and stamp an empty hash on the execution.
        await().atMost(15, SECONDS).untilAsserted(() -> {
            ResponseEntity<String> probe = restTemplate.exchange(
                    "/api/v1/environments/default/apps/test-group/routes/diagram-link-route/diagram",
                    HttpMethod.GET,
                    new HttpEntity<>(viewerHeaders),
                    String.class);
            assertThat(probe.getStatusCode()).isEqualTo(HttpStatus.OK);
        });

        String executionJson = """
                {
                  "exchangeId": "ex-diag-link-1",
                  "applicationId": "test-group",
                  "instanceId": "%s",
                  "routeId": "diagram-link-route",
                  "correlationId": "corr-diag-link-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
                    }
                  ]
                }
                """.formatted(agentId);

        ResponseEntity<String> execResponse = restTemplate.postForEntity(
                "/api/v1/data/executions",
                new HttpEntity<>(executionJson, authHeaders),
                String.class);
        assertThat(execResponse.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);

        await().atMost(15, SECONDS).untilAsserted(() -> {
            String hash = fetchDiagramContentHashByCorrelationId("corr-diag-link-1");
            assertThat(hash)
                    .as("diagram_content_hash on linked execution")
                    .isNotNull()
                    .isNotEmpty()
                    .hasSize(64)
                    .matches("[a-f0-9]{64}");
        });
    }

    @Test
    void diagramHashEmpty_whenNoRouteGraphExists() throws Exception {
        String executionJson = """
                {
                  "exchangeId": "ex-no-diag-1",
                  "applicationId": "test-group",
                  "instanceId": "%s",
                  "routeId": "no-diagram-route",
                  "correlationId": "corr-no-diag-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-no-diag",
                      "processorType": "log",
                      "status": "COMPLETED",
                      "startTime": "2026-03-11T10:00:00Z",
                      "durationMs": 500
                    }
                  ]
                }
                """.formatted(agentId);

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/data/executions",
                new HttpEntity<>(executionJson, authHeaders),
                String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);

        await().atMost(15, SECONDS).untilAsserted(() -> {
            String hash = fetchDiagramContentHashByCorrelationId("corr-no-diag-1");
            assertThat(hash)
                    .as("diagram_content_hash on un-linked execution")
                    .isNotNull()
                    .isEmpty();
        });
    }

    /**
     * Returns the {@code diagramContentHash} field off the execution-detail
     * REST response, or null if the execution isn't visible yet. Forces the
     * assertion pipeline to go controller→service→store rather than a raw
     * SQL read against ClickHouse.
     */
    private String fetchDiagramContentHashByCorrelationId(String correlationId) throws Exception {
        ResponseEntity<String> search = restTemplate.exchange(
                "/api/v1/environments/default/executions?correlationId=" + correlationId,
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class);
        if (search.getStatusCode() != HttpStatus.OK) return null;
        JsonNode body = objectMapper.readTree(search.getBody());
        if (body.get("total").asLong() < 1) return null;
        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);
        if (detail.getStatusCode() != HttpStatus.OK) return null;
        JsonNode detailBody = objectMapper.readTree(detail.getBody());
        JsonNode field = detailBody.path("diagramContentHash");
        // JSON null → empty string, mirroring how the ingestion service
        // stamps "" on executions with no linked diagram.
        return field.isMissingNode() || field.isNull() ? "" : field.asText();
    }
}
