package cn.nianxx.thhotel.integrations.ai.superagent.service.impl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import cn.nianxx.thhotel.integrations.ai.superagent.common.request.SuperAgentMailDebugRequest;
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiResult;
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiTraceEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpServer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import org.junit.jupiter.api.Test;

class SuperAgentOpenApiClientImplTest {

    @Test
    void shouldRequestStreamingMessagesWithIncludeTraceAndEmitPublicTraceEvents() throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
        AtomicReference<String> streamQuery = new AtomicReference<>();
        AtomicReference<String> sessionCsrfToken = new AtomicReference<>();
        AtomicReference<String> sessionCookie = new AtomicReference<>();
        AtomicReference<String> streamCsrfToken = new AtomicReference<>();
        AtomicReference<String> streamCookie = new AtomicReference<>();
        server.createContext("/api/open/agent-sessions", exchange -> {
            sessionCsrfToken.set(exchange.getRequestHeaders().getFirst("X-CSRF-Token"));
            sessionCookie.set(exchange.getRequestHeaders().getFirst("Cookie"));
            byte[] response = "{\"session_id\":\"session-http-001\"}".getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.createContext("/api/open/agent-sessions/session-http-001/messages/stream", exchange -> {
            streamQuery.set(exchange.getRequestURI().getRawQuery());
            streamCsrfToken.set(exchange.getRequestHeaders().getFirst("X-CSRF-Token"));
            streamCookie.set(exchange.getRequestHeaders().getFirst("Cookie"));
            byte[] response = """
                    id: 1
                    event: trace
                    data: {"event":"reasoning.summary","run_id":"run-http-001","text":"正在分析 Debug 邮件。","ts":"2026-07-11T10:00:01Z"}

                    id: 2
                    event: trace
                    data: {"event":"message.final","run_id":"run-http-001","text":"{\\"route_code\\":\\"S10\\"}"}

                    id: 3
                    event: trace
                    data: {"event":"run.completed","run_id":"run-http-001","status":"success"}

                    id: 4
                    event: end
                    data: {}

                    """.getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "text/event-stream");
            exchange.getResponseHeaders().add("Content-Location", "/api/open/agent-sessions/session-http-001/runs/run-http-001");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.start();
        try {
            SuperAgentOpenApiProperties properties = new SuperAgentOpenApiProperties();
            properties.setEnabled(true);
            properties.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
            properties.setApiKey("df_open_test");
            properties.setExternalSubjectId("debug-subject");
            properties.setConnectTimeout(Duration.ofSeconds(5));
            properties.setReadTimeout(Duration.ofSeconds(5));
            SuperAgentOpenApiClientImpl client = new SuperAgentOpenApiClientImpl(
                    properties,
                    new SuperAgentOpenApiSseParser(new ObjectMapper()),
                    new ObjectMapper());
            List<SuperAgentOpenApiTraceEvent> traceEvents = new ArrayList<>();

            SuperAgentOpenApiResult result = client.invokeMailDebug(new SuperAgentMailDebugRequest(
                    "debug message",
                    "debug-idempotency",
                    Map.of("source", "unit-test")), traceEvents::add);

            assertThat(streamQuery.get()).isEqualTo("include_trace=true");
            assertThat(sessionCsrfToken.get()).isNotBlank();
            assertThat(sessionCookie.get()).contains("csrf_token=" + sessionCsrfToken.get());
            assertThat(streamCsrfToken.get()).isNotBlank();
            assertThat(streamCookie.get()).contains("csrf_token=" + streamCsrfToken.get());
            assertThat(result.sessionId()).isEqualTo("session-http-001");
            assertThat(result.runId()).isEqualTo("run-http-001");
            assertThat(result.lastEventId()).isEqualTo("4");
            assertThat(result.traceEvents()).hasSize(3);
            assertThat(traceEvents).hasSize(3);
            assertThat(traceEvents.get(0).event()).isEqualTo("reasoning.summary");
            assertThat(traceEvents.get(0).text()).isEqualTo("正在分析 Debug 邮件。");
        } finally {
            server.stop(0);
        }
    }

    @Test
    void shouldRecoverPrematureEofBySubscribingRunEventsWithoutRepostingInitialMessage() throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
        AtomicInteger initialMessagePostCount = new AtomicInteger();
        AtomicReference<String> lastEventId = new AtomicReference<>();
        server.createContext("/api/open/agent-sessions", exchange -> {
            byte[] response = "{\"session_id\":\"session-recover-001\"}".getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.createContext("/api/open/agent-sessions/session-recover-001/messages/stream", exchange -> {
            initialMessagePostCount.incrementAndGet();
            byte[] response = """
                    id: 1
                    event: trace
                    data: {"event":"message.delta","run_id":"run-recover-001","text":"{\\"route"}

                    """.getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "text/event-stream");
            exchange.getResponseHeaders().add("Content-Location", "/api/open/agent-sessions/session-recover-001/runs/run-recover-001");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.createContext("/api/open/agent-sessions/session-recover-001/runs/run-recover-001", exchange -> {
            byte[] response = "{\"status\":\"running\"}".getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.createContext("/api/open/agent-sessions/session-recover-001/runs/run-recover-001/events", exchange -> {
            lastEventId.set(exchange.getRequestHeaders().getFirst("Last-Event-ID"));
            byte[] response = """
                    id: 2
                    event: trace
                    data: {"event":"message.final","run_id":"run-recover-001","text":"{\\"route_code\\":\\"S10\\"}"}

                    id: 3
                    event: trace
                    data: {"event":"run.completed","run_id":"run-recover-001","status":"success"}

                    id: 4
                    event: end
                    data: {}

                    """.getBytes(StandardCharsets.UTF_8);
            exchange.getResponseHeaders().add("Content-Type", "text/event-stream");
            exchange.sendResponseHeaders(200, response.length);
            try (OutputStream outputStream = exchange.getResponseBody()) {
                outputStream.write(response);
            }
        });
        server.start();
        try {
            SuperAgentOpenApiProperties properties = new SuperAgentOpenApiProperties();
            properties.setEnabled(true);
            properties.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
            properties.setApiKey("df_open_test");
            properties.setExternalSubjectId("debug-subject");
            properties.setConnectTimeout(Duration.ofSeconds(5));
            properties.setReadTimeout(Duration.ofSeconds(5));
            properties.setSseRecoveryMaxAttempts(2);
            SuperAgentOpenApiClientImpl client = new SuperAgentOpenApiClientImpl(
                    properties,
                    new SuperAgentOpenApiSseParser(new ObjectMapper()),
                    new ObjectMapper());

            SuperAgentOpenApiResult result = client.invokeMailDebug(new SuperAgentMailDebugRequest(
                    "debug message",
                    "debug-recover-idempotency",
                    Map.of("source", "unit-test")));

            assertThat(initialMessagePostCount).hasValue(1);
            assertThat(lastEventId).hasValue("1");
            assertThat(result.rawAnswer()).isEqualTo("{\"route_code\":\"S10\"}");
            assertThat(result.runId()).isEqualTo("run-recover-001");
            assertThat(result.lastEventId()).isEqualTo("4");
        } finally {
            server.stop(0);
        }
    }

    @Test
    void shouldCloseStreamingResponseBodyWhenStatusIsNot2xx() {
        TrackingInputStream responseBody = new TrackingInputStream("bad gateway".getBytes(StandardCharsets.UTF_8));
        FakeHttpClient httpClient = new FakeHttpClient(responseBody);
        SuperAgentOpenApiClientImpl client = new SuperAgentOpenApiClientImpl(
                openApiProperties(),
                new SuperAgentOpenApiSseParser(new ObjectMapper()),
                new ObjectMapper(),
                httpClient);

        assertThatThrownBy(() -> client.invokeMailDebug(new SuperAgentMailDebugRequest(
                "debug message",
                "debug-500-idempotency",
                Map.of("source", "unit-test"))))
                .isInstanceOf(SuperAgentOpenApiException.class)
                .hasMessageContaining("status=502");
        assertThat(responseBody.closed()).isTrue();
        assertThat(httpClient.streamPostCount()).isEqualTo(1);
    }

    private SuperAgentOpenApiProperties openApiProperties() {
        SuperAgentOpenApiProperties properties = new SuperAgentOpenApiProperties();
        properties.setEnabled(true);
        properties.setBaseUrl("http://superagent.test");
        properties.setApiKey("df_open_test");
        properties.setExternalSubjectId("debug-subject");
        return properties;
    }

    private static final class TrackingInputStream extends ByteArrayInputStream {
        private boolean closed;

        private TrackingInputStream(byte[] buffer) {
            super(buffer);
        }

        @Override
        public void close() throws IOException {
            closed = true;
            super.close();
        }

        private boolean closed() {
            return closed;
        }
    }

    private static final class FakeHttpClient extends HttpClient {
        private final TrackingInputStream streamingBody;
        private final AtomicInteger streamPostCount = new AtomicInteger();

        private FakeHttpClient(TrackingInputStream streamingBody) {
            this.streamingBody = streamingBody;
        }

        private int streamPostCount() {
            return streamPostCount.get();
        }

        @Override
        public Optional<CookieHandler> cookieHandler() {
            return Optional.empty();
        }

        @Override
        public Optional<Duration> connectTimeout() {
            return Optional.empty();
        }

        @Override
        public Redirect followRedirects() {
            return Redirect.NEVER;
        }

        @Override
        public Optional<ProxySelector> proxy() {
            return Optional.empty();
        }

        @Override
        public SSLContext sslContext() {
            try {
                return SSLContext.getDefault();
            } catch (NoSuchAlgorithmException exception) {
                throw new IllegalStateException(exception);
            }
        }

        @Override
        public SSLParameters sslParameters() {
            return new SSLParameters();
        }

        @Override
        public Optional<Authenticator> authenticator() {
            return Optional.empty();
        }

        @Override
        public Version version() {
            return Version.HTTP_1_1;
        }

        @Override
        public Optional<Executor> executor() {
            return Optional.empty();
        }

        @Override
        public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
            String path = request.uri().getPath();
            if (path.endsWith("/api/open/agent-sessions")) {
                return response(request, 200, "{\"session_id\":\"session-http-500\"}");
            }
            if (path.endsWith("/api/open/agent-sessions/session-http-500/messages/stream")) {
                streamPostCount.incrementAndGet();
                return response(request, 502, streamingBody);
            }
            throw new AssertionError("Unexpected request path: " + path);
        }

        @Override
        public <T> CompletableFuture<HttpResponse<T>> sendAsync(
                HttpRequest request,
                HttpResponse.BodyHandler<T> responseBodyHandler) {
            return CompletableFuture.failedFuture(new UnsupportedOperationException());
        }

        @Override
        public <T> CompletableFuture<HttpResponse<T>> sendAsync(
                HttpRequest request,
                HttpResponse.BodyHandler<T> responseBodyHandler,
                HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
            return CompletableFuture.failedFuture(new UnsupportedOperationException());
        }

        @SuppressWarnings("unchecked")
        private <T> HttpResponse<T> response(HttpRequest request, int statusCode, Object body) {
            return new SimpleHttpResponse<>(request, statusCode, (T) body);
        }
    }

    private record SimpleHttpResponse<T>(
            HttpRequest request,
            int statusCode,
            T body
    ) implements HttpResponse<T> {

        @Override
        public Optional<HttpResponse<T>> previousResponse() {
            return Optional.empty();
        }

        @Override
        public HttpHeaders headers() {
            return HttpHeaders.of(Map.of(), (name, value) -> true);
        }

        @Override
        public Optional<SSLSession> sslSession() {
            return Optional.empty();
        }

        @Override
        public java.net.URI uri() {
            return request.uri();
        }

        @Override
        public HttpClient.Version version() {
            return HttpClient.Version.HTTP_1_1;
        }
    }
}
