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.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 tests for {@link DiagramRenderController}. The env-scoped
 * endpoint only serves JSON — SVG rendering is only available via the
 * flat content-hash endpoint. We seed the diagram plus an execution for
 * the same route, then pull the content hash from the execution-detail
 * REST response to drive the flat-endpoint render tests.
 */
class DiagramRenderControllerIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestSecurityHelper securityHelper;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private String jwt;
    private String viewerJwt;
    private String contentHash;

    @BeforeEach
    void seedDiagram() {
        // 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-diagram-render-it");
        viewerJwt = securityHelper.viewerToken();

        String diagramJson = """
                {
                  "routeId": "render-test-route",
                  "description": "Render test",
                  "version": 1,
                  "nodes": [
                    {"id": "n1", "type": "ENDPOINT", "label": "timer:tick"},
                    {"id": "n2", "type": "BEAN", "label": "myBean"},
                    {"id": "n3", "type": "TO", "label": "log:out"}
                  ],
                  "edges": [
                    {"source": "n1", "target": "n2", "edgeType": "FLOW"},
                    {"source": "n2", "target": "n3", "edgeType": "FLOW"}
                  ]
                }
                """;
        restTemplate.postForEntity(
                "/api/v1/data/diagrams",
                new HttpEntity<>(diagramJson, securityHelper.authHeaders(jwt)),
                String.class);

        // Post an execution for the same route so the ingestion pipeline
        // stamps diagramContentHash on it — that's our path to fetching the
        // hash without reading route_diagrams directly.
        String execJson = """
                {
                  "exchangeId": "render-probe-exchange",
                  "applicationId": "test-group",
                  "instanceId": "test-agent-diagram-render-it",
                  "routeId": "render-test-route",
                  "correlationId": "render-probe-corr",
                  "status": "COMPLETED",
                  "startTime": "2026-03-11T10:00:00Z",
                  "endTime": "2026-03-11T10:00:01Z",
                  "durationMs": 1000,
                  "chunkSeq": 0,
                  "final": true,
                  "processors": []
                }
                """;
        restTemplate.postForEntity(
                "/api/v1/data/executions",
                new HttpEntity<>(execJson, securityHelper.authHeaders(jwt)),
                String.class);

        // Wait for both to land, then read the hash off the execution detail.
        await().atMost(20, SECONDS).untilAsserted(() -> {
            HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
            ResponseEntity<String> search = restTemplate.exchange(
                    "/api/v1/environments/default/executions?correlationId=render-probe-corr",
                    HttpMethod.GET,
                    new HttpEntity<>(headers),
                    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<>(headers),
                    String.class);
            assertThat(detail.getStatusCode()).isEqualTo(HttpStatus.OK);
            JsonNode detailBody = objectMapper.readTree(detail.getBody());
            String hash = detailBody.path("diagramContentHash").asText();
            assertThat(hash).isNotEmpty();
            contentHash = hash;
        });
    }

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

    @Test
    void getSvg_withAcceptHeader_returnsSvg() {
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        headers.set("Accept", "image/svg+xml");

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class,
                contentHash);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getHeaders().getContentType().toString()).contains("svg");
        assertThat(response.getBody()).contains("<svg");
    }

    @Test
    void getJson_withAcceptHeader_returnsJson() {
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        headers.set("Accept", "application/json");

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class,
                contentHash);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("nodes");
        assertThat(response.getBody()).contains("edges");
    }

    @Test
    void getNonExistentHash_returns404() {
        // Only test the flat content-hash endpoint here — 404 on bogus hash
        // doesn't need a valid hash, so no SQL lookup is required.
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        headers.set("Accept", "image/svg+xml");

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class,
                "nonexistent-hash-12345");

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

    @Test
    void findByAppAndRoute_returnsLatestDiagram_noLiveAgentPrereq() {
        // The env-scoped /routes/{routeId}/diagram endpoint no longer depends
        // on the agent registry — routes whose publishing agents have been
        // removed must still resolve. The seed step stored a diagram for
        // route "render-test-route" under app "test-group" / env "default",
        // so the same lookup must succeed even though the registry-driven
        // "find agents for app" path used to be a hard 404 prerequisite.
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        headers.set("Accept", "application/json");

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/environments/default/apps/test-group/routes/render-test-route/diagram",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("nodes");
        assertThat(response.getBody()).contains("edges");
    }

    @Test
    void findByAppAndRoute_returns404ForUnknownRoute() {
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);
        headers.set("Accept", "application/json");

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/environments/default/apps/test-group/routes/nonexistent-route/diagram",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class);

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

    @Test
    void exchangeDiagramHash_pinsPointInTimeEvenAfterNewerVersion() throws Exception {
        // Point-in-time guarantee: an execution's stored diagramContentHash
        // must keep resolving to the route shape captured at execution time,
        // even after a newer diagram version for the same route is stored.
        // Content-hash addressing + never-delete of route_diagrams makes this
        // automatic — this test locks the invariant in.
        HttpHeaders viewerHeaders = securityHelper.authHeadersNoBody(viewerJwt);
        viewerHeaders.set("Accept", "application/json");

        // Snapshot the pinned v1 render via the flat content-hash endpoint
        // BEFORE a newer version is stored, so the post-v2 fetch can compare
        // byte-for-byte.
        ResponseEntity<String> pinnedBefore = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class,
                contentHash);
        assertThat(pinnedBefore.getStatusCode()).isEqualTo(HttpStatus.OK);

        // Also snapshot the by-route "latest" render for the same route.
        ResponseEntity<String> latestBefore = restTemplate.exchange(
                "/api/v1/environments/default/apps/test-group/routes/render-test-route/diagram",
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class);
        assertThat(latestBefore.getStatusCode()).isEqualTo(HttpStatus.OK);

        // Store a materially different v2 for the same (app, env, route).
        // The renderer walks the `root` tree (not the legacy flat `nodes`
        // list that the seed payload uses), so v2 uses the tree shape and
        // will render non-empty output — letting us detect the version flip.
        String newerDiagramJson = """
                {
                  "routeId": "render-test-route",
                  "description": "v2 with extra step",
                  "version": 2,
                  "root": {
                    "id": "n1",
                    "type": "ENDPOINT",
                    "label": "timer:tick-v2",
                    "children": [
                      {
                        "id": "n2",
                        "type": "BEAN",
                        "label": "myBeanV2",
                        "children": [
                          {
                            "id": "n3",
                            "type": "TO",
                            "label": "log:out-v2",
                            "children": [
                              {"id": "n4", "type": "TO", "label": "log:audit"}
                            ]
                          }
                        ]
                      }
                    ]
                  },
                  "edges": [
                    {"source": "n1", "target": "n2", "edgeType": "FLOW"},
                    {"source": "n2", "target": "n3", "edgeType": "FLOW"},
                    {"source": "n3", "target": "n4", "edgeType": "FLOW"}
                  ]
                }
                """;
        restTemplate.postForEntity(
                "/api/v1/data/diagrams",
                new HttpEntity<>(newerDiagramJson, securityHelper.authHeaders(jwt)),
                String.class);

        // Invariant 1: The execution's stored diagramContentHash must not
        // drift — exchanges stay pinned to the version captured at ingest.
        ResponseEntity<String> detailAfter = restTemplate.exchange(
                "/api/v1/environments/default/executions?correlationId=render-probe-corr",
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class);
        JsonNode search = objectMapper.readTree(detailAfter.getBody());
        String execId = search.get("data").get(0).get("executionId").asText();
        ResponseEntity<String> exec = restTemplate.exchange(
                "/api/v1/executions/" + execId,
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class);
        JsonNode execBody = objectMapper.readTree(exec.getBody());
        assertThat(execBody.path("diagramContentHash").asText()).isEqualTo(contentHash);

        // Invariant 2: The pinned render (by H1) must be byte-identical
        // before and after v2 is stored — content-hash addressing is stable.
        ResponseEntity<String> pinnedAfter = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(viewerHeaders),
                String.class,
                contentHash);
        assertThat(pinnedAfter.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(pinnedAfter.getBody()).isEqualTo(pinnedBefore.getBody());

        // Invariant 3: The by-route "latest" endpoint must now surface v2,
        // so its body differs from the pre-v2 snapshot. Retry briefly to
        // absorb the diagram-ingest flush path.
        await().atMost(20, SECONDS).untilAsserted(() -> {
            ResponseEntity<String> latestAfter = restTemplate.exchange(
                    "/api/v1/environments/default/apps/test-group/routes/render-test-route/diagram",
                    HttpMethod.GET,
                    new HttpEntity<>(viewerHeaders),
                    String.class);
            assertThat(latestAfter.getStatusCode()).isEqualTo(HttpStatus.OK);
            assertThat(latestAfter.getBody()).isNotEqualTo(latestBefore.getBody());
            assertThat(latestAfter.getBody()).contains("myBeanV2");
        });
    }

    @Test
    void getWithNoAcceptHeader_defaultsToSvg() {
        HttpHeaders headers = securityHelper.authHeadersNoBody(viewerJwt);

        ResponseEntity<String> response = restTemplate.exchange(
                "/api/v1/diagrams/{hash}/render",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                String.class,
                contentHash);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("<svg");
    }
}
