package fuck.andes.agent.model

import com.sun.net.httpserver.HttpServer
import fuck.andes.agent.runtime.AgentRunController
import fuck.andes.data.model.CustomBody
import fuck.andes.data.model.ModelReasoningCapabilities
import fuck.andes.data.model.OpenAiEndpointMode
import fuck.andes.data.model.ReasoningEffort
import java.net.InetSocketAddress
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
import kotlinx.serialization.json.JsonPrimitive
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

class OpenAiResponsesProviderTest {
    @Test
    fun requestUsesTypedInputProtectedFieldsAndOptionalHostedSearch() {
        val assistant = JSONObject().put("role", "assistant").put("content", "先检查")
        ResponsesEphemeralState.attachOutputItems(
            assistant,
            JSONArray().put(
                JSONObject()
                    .put("id", "rs_1")
                    .put("type", "reasoning")
                    .put("encrypted_content", "opaque"),
            ),
        )
        val messages = JSONArray()
            .put(
                JSONObject().put("role", "user").put(
                    "content",
                    JSONArray()
                        .put(JSONObject().put("type", "text").put("text", "看图"))
                        .put(
                            JSONObject()
                                .put("type", "image_url")
                                .put("image_url", JSONObject().put("url", "data:image/png;base64,AA==")),
                        ),
                ),
            )
            .put(assistant)
            .put(
                JSONObject()
                    .put("role", "tool")
                    .put("tool_call_id", "call_1")
                    .put("content", "{\"ok\":true}"),
            )
        val tools = JSONArray().put(
            JSONObject().put("type", "function").put(
                "function",
                JSONObject()
                    .put("name", "device_info")
                    .put("description", "读取设备")
                    .put("parameters", JSONObject().put("type", "object")),
            ),
        )

        val request = OpenAiResponsesProvider.buildRequestJson(
            config = config("https://example.com/v1").copy(
                hostedWebSearchEnabled = true,
                reasoningCapabilities = ModelReasoningCapabilities(
                    supportedEfforts = listOf(ReasoningEffort.HIGH),
                    canDisable = true,
                ),
                reasoningEffort = ReasoningEffort.HIGH,
                extraBodyJson = """{"model":"wrong","store":true,"metadata":{"source":"eta"}}""",
                customBody = listOf(CustomBody("stream", JsonPrimitive(false))),
            ),
            messages = messages,
            tools = tools,
        )

        assertEquals("test-model", request.getString("model"))
        assertEquals("系统提示", request.getString("instructions"))
        assertTrue(request.getBoolean("stream"))
        assertFalse(request.getBoolean("store"))
        assertFalse(request.has("previous_response_id"))
        assertEquals("eta", request.getJSONObject("metadata").getString("source"))
        assertEquals("high", request.getJSONObject("reasoning").getString("effort"))
        assertEquals("auto", request.getJSONObject("reasoning").getString("summary"))
        assertEquals("input_image", request.getJSONArray("input").getJSONObject(0)
            .getJSONArray("content").getJSONObject(1).getString("type"))
        assertEquals("reasoning", request.getJSONArray("input").getJSONObject(1).getString("type"))
        assertEquals("function_call_output", request.getJSONArray("input").getJSONObject(2).getString("type"))
        assertFalse(request.getJSONArray("tools").getJSONObject(0).getBoolean("strict"))
        assertEquals("web_search", request.getJSONArray("tools").getJSONObject(1).getString("type"))
    }

    @Test
    fun completeUsesTerminalOutputAsAuthorityAndEmitsSemanticEvents() {
        val terminalOutput = JSONArray()
            .put(
                JSONObject()
                    .put("id", "rs_1")
                    .put("type", "reasoning")
                    .put("encrypted_content", "opaque")
                    .put("summary", JSONArray().put(JSONObject().put("type", "summary_text").put("text", "完整摘要"))),
            )
            .put(JSONObject().put("id", "ws_1").put("type", "web_search_call").put("status", "completed"))
            .put(
                JSONObject()
                    .put("id", "msg_1")
                    .put("type", "message")
                    .put(
                        "content",
                        JSONArray().put(
                            JSONObject()
                                .put("type", "output_text")
                                .put("text", "你好世界")
                                .put(
                                    "annotations",
                                    JSONArray().put(
                                        JSONObject()
                                            .put("type", "url_citation")
                                            .put("start_index", 2)
                                            .put("end_index", 4)
                                            .put("url", "https://example.com/source")
                                            .put("title", "来源"),
                                    ),
                                ),
                        ),
                    ),
            )
            .put(
                JSONObject()
                    .put("id", "fc_1")
                    .put("type", "function_call")
                    .put("call_id", "call_1")
                    .put("name", "device_info")
                    .put("arguments", "{\"detail\":true}"),
            )
        val response = JSONObject()
            .put("status", "completed")
            .put("output", terminalOutput)
            .put(
                "usage",
                JSONObject()
                    .put("input_tokens", 10)
                    .put("output_tokens", 8)
                    .put("total_tokens", 18)
                    .put("input_tokens_details", JSONObject().put("cached_tokens", 3))
                    .put("output_tokens_details", JSONObject().put("reasoning_tokens", 5)),
            )
        val body = buildString {
            append(event("response.reasoning_summary_text.delta", JSONObject().put("delta", "完整")))
            append(
                event(
                    "response.output_item.added",
                    JSONObject().put("item", JSONObject().put("id", "ws_1").put("type", "web_search_call")),
                ),
            )
            append(
                event(
                    "response.output_item.done",
                    JSONObject().put(
                        "item",
                        JSONObject().put("id", "ws_1").put("type", "web_search_call").put("status", "completed"),
                    ),
                ),
            )
            append(event("response.output_text.delta", JSONObject().put("delta", "你好")))
            append(event("response.completed", JSONObject().put("response", response)))
        }
        val requestBody = AtomicReference<String>()

        withSseServer(body, onRequest = requestBody::set) { baseUrl ->
            val events = mutableListOf<ProviderEvent>()
            val result = OpenAiResponsesProvider.complete(
                request = ProviderRequest(
                    config = config(baseUrl),
                    messages = JSONArray().put(JSONObject().put("role", "user").put("content", "你好")),
                    tools = JSONArray(),
                ),
                runController = AgentRunController(),
                onEvent = events::add,
            )

            assertTrue(requestBody.get().contains("\"store\":false"))
            assertEquals("完整摘要", result.assistantMessage.getString("reasoning_content"))
            assertTrue(result.assistantMessage.getString("content").contains("[[1]]"))
            assertEquals("call_1", result.assistantMessage.getJSONArray("tool_calls").getJSONObject(0).getString("id"))
            assertNotNull(ResponsesEphemeralState.outputItems(result.assistantMessage))
            assertEquals(1, events.filterIsInstance<ProviderEvent.HostedToolStarted>().size)
            assertEquals(1, events.filterIsInstance<ProviderEvent.HostedToolFinished>().size)
            val usage = events.filterIsInstance<ProviderEvent.Usage>().single().usage
            assertEquals(18, usage.contextTokens)
            assertEquals(3, usage.cachedTokens)
            assertEquals(5, usage.reasoningTokens)
        }
    }

    @Test
    fun completeUsesStreamedTextWhenCompletedOutputIsEmpty() {
        val body = buildString {
            append(event("response.reasoning_summary_text.delta", JSONObject().put("delta", "简要分析")))
            append(event("response.output_text.delta", JSONObject().put("delta", "你好")))
            append(event("response.output_text.delta", JSONObject().put("delta", "世界")))
            append(
                event(
                    "response.completed",
                    JSONObject().put(
                        "response",
                        JSONObject().put("status", "completed"),
                    ),
                ),
            )
        }

        withSseServer(body) { baseUrl ->
            val result = OpenAiResponsesProvider.complete(
                ProviderRequest(
                    config(baseUrl),
                    JSONArray().put(JSONObject().put("role", "user").put("content", "你好")),
                    JSONArray(),
                ),
                AgentRunController(),
            )

            assertEquals("你好世界", result.assistantMessage.getString("content"))
            assertEquals("简要分析", result.assistantMessage.getString("reasoning_content"))
            assertEquals("stop", result.assistantMessage.getString("finish_reason"))
            assertNull(ResponsesEphemeralState.outputItems(result.assistantMessage))
        }
    }

    @Test
    fun completeUsesStreamedToolCallWhenCompletedOutputIsEmpty() {
        val body = buildString {
            append(
                event(
                    "response.output_item.added",
                    JSONObject()
                        .put("output_index", 0)
                        .put(
                            "item",
                            JSONObject()
                                .put("id", "fc_1")
                                .put("type", "function_call")
                                .put("call_id", "call_1")
                                .put("name", "device_info"),
                        ),
                ),
            )
            append(
                event(
                    "response.function_call_arguments.delta",
                    JSONObject().put("item_id", "fc_1").put("delta", "{\"detail\":true}"),
                ),
            )
            append(
                event(
                    "response.completed",
                    JSONObject().put(
                        "response",
                        JSONObject().put("status", "completed").put("output", JSONArray()),
                    ),
                ),
            )
        }

        withSseServer(body) { baseUrl ->
            val result = OpenAiResponsesProvider.complete(
                ProviderRequest(
                    config(baseUrl),
                    JSONArray().put(JSONObject().put("role", "user").put("content", "读取设备")),
                    JSONArray(),
                ),
                AgentRunController(),
            )

            val call = result.assistantMessage.getJSONArray("tool_calls").getJSONObject(0)
            assertEquals("call_1", call.getString("id"))
            assertEquals("device_info", call.getJSONObject("function").getString("name"))
            assertEquals("{\"detail\":true}", call.getJSONObject("function").getString("arguments"))
            assertEquals("tool_calls", result.assistantMessage.getString("finish_reason"))
            assertNull(ResponsesEphemeralState.outputItems(result.assistantMessage))
        }
    }

    @Test
    fun completeRejectsEofWithoutTerminalEvent() {
        withSseServer(event("response.output_text.delta", JSONObject().put("delta", "partial"))) { baseUrl ->
            val thrown = runCatching {
                OpenAiResponsesProvider.complete(
                    ProviderRequest(
                        config(baseUrl),
                        JSONArray().put(JSONObject().put("role", "user").put("content", "hi")),
                        JSONArray(),
                    ),
                    AgentRunController(),
                )
            }.exceptionOrNull()
            assertTrue(thrown?.message.orEmpty().contains("缺少合法终止事件"))
        }
    }

    @Test
    fun citationsDeduplicateAndFallBackForInvalidOffsets() {
        val formatted = ResponsesCitationFormatter.apply(
            "中文回答",
            listOf(
                CitationAnnotation(0, 2, "https://example.com/a", "A"),
                CitationAnnotation(0, 2, "https://example.com/a", "重复"),
                CitationAnnotation(99, 120, "https://example.com/b", "B"),
            ),
        )
        assertEquals(1, "https://example.com/a".toRegex().findAll(formatted).count())
        assertTrue(formatted.contains("[[1]]"))
        assertTrue(formatted.contains("来源："))
        assertTrue(formatted.contains("https://example.com/b"))
    }

    private fun config(baseUrl: String) = AgentModelClient.ModelConfig(
        providerSourceType = "custom",
        baseUrl = baseUrl,
        apiKey = "test-key",
        model = "test-model",
        systemPrompt = "系统提示",
        openAiEndpointMode = OpenAiEndpointMode.RESPONSES,
    )

    private fun event(type: String, fields: JSONObject): String {
        val event = JSONObject(fields.toString()).put("type", type)
        return "event: $type\ndata: $event\n\n"
    }

    private fun withSseServer(
        body: String,
        onRequest: (String) -> Unit = {},
        block: (String) -> Unit,
    ) {
        val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
        val executor = Executors.newSingleThreadExecutor()
        server.executor = executor
        server.createContext("/responses") { exchange ->
            onRequest(exchange.requestBody.use { it.readBytes().toString(Charsets.UTF_8) })
            val bytes = body.toByteArray(Charsets.UTF_8)
            exchange.responseHeaders.add("Content-Type", "text/event-stream")
            exchange.sendResponseHeaders(200, bytes.size.toLong())
            exchange.responseBody.use { it.write(bytes) }
        }
        server.start()
        try {
            block("http://127.0.0.1:${server.address.port}")
        } finally {
            server.stop(0)
            executor.shutdownNow()
        }
    }
}
