package com.cameleer.server.app.controller;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
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.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.time.Duration;
import java.util.ArrayList;
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;

class AgentSseControllerIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @LocalServerPort
    private int port;

    private String jwt;
    private String operatorJwt;

    @BeforeEach
    void setUp() {
        // Lift max_agents cap so this IT (which registers many agents per test) 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-sse-it");
        operatorJwt = securityHelper.operatorToken();
    }

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

    private ResponseEntity<String> registerAgent(String agentId, String name, String application) {
        String json = """
                {
                  "instanceId": "%s",
                  "applicationId": "%s",
                  "environmentId": "default",
                  "version": "1.0.0",
                  "routeIds": ["route-1"],
                  "capabilities": {}
                }
                """.formatted(agentId, application);

        return restTemplate.postForEntity(
                "/api/v1/agents/register",
                new HttpEntity<>(json, securityHelper.bootstrapHeaders()),
                String.class);
    }

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

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

    /**
     * Opens an SSE stream via java.net.http.HttpClient with JWT query param auth.
     */
    private SseStream openSseStream(String agentId) {
        return openSseStream(agentId, null);
    }

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

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

        // Use JWT query parameter for SSE authentication
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:" + port + "/api/v1/agents/" + agentId + "/events?token=" + jwt))
                .header("Accept", "text/event-stream")
                .GET();

        if (lastEventId != null) {
            requestBuilder.header("Last-Event-ID", lastEventId);
        }

        HttpRequest request = requestBuilder.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 sseConnect_registeredAgent_returnsEventStream() throws Exception {
        String agentId = "sse-it-connect-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "SSE Connect Agent", "test-group");

        SseStream stream = openSseStream(agentId);

        assertThat(stream.awaitConnection(5000)).isTrue();
        assertThat(stream.statusCode().get()).isEqualTo(200);
    }

    @Test
    void sseConnect_unknownAgent_returns404() throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(5))
                .build();

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

        CompletableFuture<Integer> statusFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::statusCode);

        int status = statusFuture.get(5, TimeUnit.SECONDS);
        assertThat(status).isEqualTo(404);
    }

    @Test
    void configUpdateDelivery_receivedViaSseStream() throws Exception {
        String agentId = "sse-it-config-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "Config Update Agent", "test-group");

        SseStream stream = openSseStream(agentId);
        stream.awaitConnection(5000);

        await().atMost(5, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
                .ignoreExceptions()
                .until(() -> {
                    sendCommand(agentId, "config-update", "{\"key\":\"value\"}");
                    List<String> lines = stream.snapshot();
                    return lines.stream().anyMatch(l -> l.contains("event:config-update"));
                });

        List<String> lines = stream.snapshot();
        assertThat(lines).anyMatch(l -> l.contains("event:config-update"));
        assertThat(lines).anyMatch(l -> l.startsWith("id:"));
        assertThat(lines).anyMatch(l -> l.contains("\"key\":\"value\""));
    }

    @Test
    void deepTraceDelivery_receivedViaSseStream() throws Exception {
        String agentId = "sse-it-trace-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "Deep Trace Agent", "test-group");

        SseStream stream = openSseStream(agentId);
        stream.awaitConnection(5000);

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

        List<String> lines = stream.snapshot();
        assertThat(lines).anyMatch(l -> l.contains("event:deep-trace"));
        assertThat(lines).anyMatch(l -> l.contains("test-123"));
    }

    @Test
    void replayDelivery_receivedViaSseStream() throws Exception {
        String agentId = "sse-it-replay-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "Replay Agent", "test-group");

        SseStream stream = openSseStream(agentId);
        stream.awaitConnection(5000);

        await().atMost(5, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
                .ignoreExceptions()
                .until(() -> {
                    sendCommand(agentId, "replay", "{\"exchangeId\":\"ex-456\"}");
                    List<String> lines = stream.snapshot();
                    return lines.stream().anyMatch(l -> l.contains("event:replay"));
                });

        List<String> lines = stream.snapshot();
        assertThat(lines).anyMatch(l -> l.contains("event:replay"));
        assertThat(lines).anyMatch(l -> l.contains("ex-456"));
    }

    @Test
    void pingKeepalive_receivedViaSseStream() throws Exception {
        String agentId = "sse-it-ping-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "Ping Agent", "test-group");

        SseStream stream = openSseStream(agentId);
        stream.awaitConnection(5000);

        await().atMost(5, TimeUnit.SECONDS).pollInterval(200, TimeUnit.MILLISECONDS)
                .ignoreExceptions()
                .until(() -> {
                    List<String> lines = stream.snapshot();
                    return lines.stream().anyMatch(l -> l.contains(":ping"));
                });

        List<String> lines = stream.snapshot();
        assertThat(lines).anyMatch(l -> l.contains(":ping"));
    }

    @Test
    void lastEventIdHeader_connectionSucceeds() throws Exception {
        String agentId = "sse-it-lastid-" + UUID.randomUUID().toString().substring(0, 8);
        registerAgent(agentId, "Last-Event-ID Agent", "test-group");

        SseStream stream = openSseStream(agentId, "some-previous-event-id");

        assertThat(stream.awaitConnection(5000)).isTrue();
        assertThat(stream.statusCode().get()).isEqualTo(200);
    }
}
