package com.cameleer.server.app.security;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.core.security.Ed25519SigningService;
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.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

/**
 * Integration test verifying that SSE command events carry valid Ed25519 signatures.
 * <p>
 * Flow: register agent (with bootstrap token) -> extract JWT + public key from response ->
 * open SSE stream (with JWT query param) -> push config-update command (with JWT) ->
 * receive SSE event -> verify signature field against server's Ed25519 public key.
 */
class SseSigningIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @Autowired
    private Ed25519SigningService ed25519SigningService;

    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        // Lift max_agents cap so this IT (which registers agents per test) isn't gated
        // by license enforcement. Cap behaviour itself is exercised by AgentCapEnforcementIT.
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100));
    }

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

    private HttpHeaders protocolHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("X-Cameleer-Protocol-Version", "1");
        return headers;
    }

    private HttpHeaders authProtocolHeaders(String accessToken) {
        HttpHeaders headers = protocolHeaders();
        headers.set("Authorization", "Bearer " + accessToken);
        return headers;
    }

    private HttpHeaders bootstrapHeaders() {
        HttpHeaders headers = protocolHeaders();
        headers.set("Authorization", "Bearer test-bootstrap-token");
        return headers;
    }

    /**
     * Registers an agent using the bootstrap token and returns the registration response.
     * The response contains: instanceId, sseEndpoint, accessToken, refreshToken, serverPublicKey.
     */
    private JsonNode registerAgentWithAuth(String agentId) throws Exception {
        String json = """
                {
                  "instanceId": "%s",
                  "applicationId": "test-group",
                  "environmentId": "default",
                  "version": "1.0.0",
                  "routeIds": ["route-1"],
                  "capabilities": {}
                }
                """.formatted(agentId);

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/register",
                new HttpEntity<>(json, bootstrapHeaders()),
                String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        return objectMapper.readTree(response.getBody());
    }

    private ResponseEntity<String> sendCommand(String agentId, String type, String payloadJson, String accessToken) {
        String json = """
                {"type": "%s", "payload": %s}
                """.formatted(type, payloadJson);

        return restTemplate.postForEntity(
                "/api/v1/agents/" + agentId + "/commands",
                new HttpEntity<>(json, authProtocolHeaders(accessToken)),
                String.class);
    }

    private SseStream openSseStream(String agentId, String accessToken) {
        List<String> lines = new ArrayList<>();
        CountDownLatch connected = new CountDownLatch(1);
        AtomicInteger statusCode = new AtomicInteger(0);

        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:" + port + "/api/v1/agents/" + agentId + "/events?token=" + accessToken))
                .header("Accept", "text/event-stream")
                .GET()
                .build();

        CompletableFuture<Void> future = client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
                .thenAccept(response -> {
                    statusCode.set(response.statusCode());
                    connected.countDown();
                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            synchronized (lines) {
                                lines.add(line);
                            }
                        }
                    } catch (Exception e) {
                        // Stream closed -- expected
                    }
                });

        return new SseStream(lines, future, connected, statusCode);
    }

    private record SseStream(List<String> lines, CompletableFuture<Void> future,
                              CountDownLatch connected, AtomicInteger statusCode) {
        List<String> snapshot() {
            synchronized (lines) {
                return new ArrayList<>(lines);
            }
        }

        boolean awaitConnection(long timeoutMs) throws InterruptedException {
            return connected.await(timeoutMs, TimeUnit.MILLISECONDS);
        }
    }

    @Test
    void configUpdateEvent_containsValidEd25519Signature() throws Exception {
        String agentId = "sse-sign-it-" + UUID.randomUUID().toString().substring(0, 8);
        JsonNode registration = registerAgentWithAuth(agentId);
        String accessToken = registration.get("accessToken").asText();
        String operatorToken = securityHelper.operatorToken();
        String serverPublicKey = registration.get("serverPublicKey").asText();

        SseStream stream = openSseStream(agentId, accessToken);
        assertThat(stream.awaitConnection(5000)).isTrue();
        assertThat(stream.statusCode().get()).isEqualTo(200);

        String originalPayload = "{\"key\":\"value\",\"setting\":\"enabled\"}";

        // Send config-update and wait for SSE event
        await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
                .ignoreExceptions()
                .until(() -> {
                    sendCommand(agentId, "config-update", originalPayload, operatorToken);
                    List<String> lines = stream.snapshot();
                    return lines.stream().anyMatch(l -> l.contains("event:config-update"));
                });

        // Extract the data line from SSE event
        List<String> lines = stream.snapshot();
        String dataLine = lines.stream()
                .filter(l -> l.startsWith("data:"))
                .findFirst()
                .orElseThrow(() -> new AssertionError("No data line in SSE event"));

        String eventData = dataLine.substring("data:".length());
        JsonNode eventNode = objectMapper.readTree(eventData);

        // Verify signature field exists
        assertThat(eventNode.has("signature"))
                .as("SSE event data should contain 'signature' field")
                .isTrue();

        String signatureBase64 = eventNode.get("signature").asText();
        assertThat(signatureBase64).isNotBlank();

        // Verify original payload fields are preserved
        assertThat(eventNode.get("key").asText()).isEqualTo("value");
        assertThat(eventNode.get("setting").asText()).isEqualTo("enabled");

        // Verify signature against original payload using server's public key
        byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
        PublicKey publicKey = loadPublicKey(serverPublicKey);

        Signature verifier = Signature.getInstance("Ed25519");
        verifier.initVerify(publicKey);
        verifier.update(originalPayload.getBytes(StandardCharsets.UTF_8));
        assertThat(verifier.verify(signatureBytes))
                .as("Ed25519 signature should verify against original payload")
                .isTrue();
    }

    @Test
    void deepTraceEvent_containsValidSignature() throws Exception {
        String agentId = "sse-sign-trace-" + UUID.randomUUID().toString().substring(0, 8);
        JsonNode registration = registerAgentWithAuth(agentId);
        String accessToken = registration.get("accessToken").asText();
        String operatorToken = securityHelper.operatorToken();
        String serverPublicKey = registration.get("serverPublicKey").asText();

        SseStream stream = openSseStream(agentId, accessToken);
        assertThat(stream.awaitConnection(5000)).isTrue();
        assertThat(stream.statusCode().get()).isEqualTo(200);

        String originalPayload = "{\"correlationId\":\"trace-123\"}";

        await().atMost(10, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
                .ignoreExceptions()
                .until(() -> {
                    sendCommand(agentId, "deep-trace", originalPayload, operatorToken);
                    List<String> lines = stream.snapshot();
                    return lines.stream().anyMatch(l -> l.contains("event:deep-trace"));
                });

        List<String> lines = stream.snapshot();
        String dataLine = lines.stream()
                .filter(l -> l.startsWith("data:"))
                .findFirst()
                .orElseThrow();

        JsonNode eventNode = objectMapper.readTree(dataLine.substring("data:".length()));
        assertThat(eventNode.has("signature")).isTrue();

        // Verify signature
        byte[] signatureBytes = Base64.getDecoder().decode(eventNode.get("signature").asText());
        PublicKey publicKey = loadPublicKey(serverPublicKey);

        Signature verifier = Signature.getInstance("Ed25519");
        verifier.initVerify(publicKey);
        verifier.update(originalPayload.getBytes(StandardCharsets.UTF_8));
        assertThat(verifier.verify(signatureBytes)).isTrue();
    }

    private PublicKey loadPublicKey(String base64PublicKey) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
        return keyFactory.generatePublic(keySpec);
    }
}
