package service

import (
	"encoding/json"
	"strings"
	"testing"

	"oci-portal/internal/aiwire"
)

func mustChatReq(t *testing.T, raw string) aiwire.ChatRequest {
	t.Helper()
	var req aiwire.ChatRequest
	if err := json.Unmarshal([]byte(raw), &req); err != nil {
		t.Fatalf("解析请求: %v", err)
	}
	return req
}

func mustChatBody(t *testing.T, raw string) map[string]any {
	t.Helper()
	payload, err := ChatToResponsesBody(mustChatReq(t, raw))
	if err != nil {
		t.Fatalf("ChatToResponsesBody: %v", err)
	}
	var body map[string]any
	if err := json.Unmarshal(payload, &body); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	return body
}

// TestChatToResponsesBody 断言消息展开、instructions 聚合、工具与顶层字段装配。
func TestChatToResponsesBody(t *testing.T) {
	body := mustChatBody(t, `{
		"model": "xai.grok-4.3", "max_tokens": 64, "max_completion_tokens": 128,
		"temperature": 0.5, "top_p": 0.9, "parallel_tool_calls": false, "stream": true,
		"reasoning_effort": "HIGH",
		"messages": [
			{"role": "system", "content": "你是助手"},
			{"role": "developer", "content": [{"type": "text", "text": "简洁作答"}]},
			{"role": "user", "content": "东京天气?"},
			{"role": "assistant", "content": "查询中", "tool_calls": [
				{"id": "t1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"东京\"}"}}
			]},
			{"role": "tool", "tool_call_id": "t1", "content": "晴 25 度"},
			{"role": "user", "content": "继续"}
		],
		"tools": [{"type": "function", "function": {"name": "get_weather", "description": "查天气", "parameters": {"type": "object"}}}],
		"tool_choice": "required"
	}`)
	if body["model"] != "xai.grok-4.3" || body["store"] != false || body["stream"] != true {
		t.Fatalf("顶层字段装配错误: %v", body)
	}
	if body["max_output_tokens"] != float64(128) {
		t.Fatalf("max_output_tokens = %v, want 128(max_completion_tokens 优先)", body["max_output_tokens"])
	}
	if body["instructions"] != "你是助手\n\n简洁作答" {
		t.Fatalf("instructions = %v", body["instructions"])
	}
	if body["temperature"] != 0.5 || body["top_p"] != 0.9 || body["parallel_tool_calls"] != false {
		t.Fatalf("采样字段装配错误: %v", body)
	}
	if body["tool_choice"] != "required" {
		t.Fatalf("tool_choice = %v", body["tool_choice"])
	}
	if reasoning, _ := body["reasoning"].(map[string]any); reasoning["effort"] != "high" {
		t.Fatalf("effort = %v, want high(小写透传)", body["reasoning"])
	}
	kinds := chatInputKinds(t, body)
	want := "message/user,message/assistant,function_call,function_call_output,message/user"
	if kinds != want {
		t.Fatalf("input 顺序 = %s, want %s", kinds, want)
	}
	tools, _ := body["tools"].([]any)
	tool, _ := tools[0].(map[string]any)
	if tool["type"] != "function" || tool["name"] != "get_weather" {
		t.Fatalf("工具应拍平为 Responses 形态: %v", tool)
	}
}

func chatInputKinds(t *testing.T, body map[string]any) string {
	t.Helper()
	input, _ := body["input"].([]any)
	kinds := make([]string, 0, len(input))
	for _, it := range input {
		m := it.(map[string]any)
		if ty, ok := m["type"].(string); ok {
			kinds = append(kinds, ty)
		} else {
			kinds = append(kinds, "message/"+m["role"].(string))
		}
	}
	return strings.Join(kinds, ",")
}

// TestChatToResponsesBodyContent 断言文本部件按角色定型与图片装配/拒绝。
func TestChatToResponsesBodyContent(t *testing.T) {
	body := mustChatBody(t, `{"model":"m","messages":[
		{"role":"user","content":[
			{"type":"text","text":"看图"},
			{"type":"image_url","image_url":{"url":"data:image/png;base64,QUJD"}}]},
		{"role":"assistant","content":"这是猫"}]}`)
	raw, _ := json.Marshal(body["input"])
	if !strings.Contains(string(raw), `"input_image"`) ||
		!strings.Contains(string(raw), "data:image/png;base64,QUJD") {
		t.Fatalf("图片应转 input_image: %s", raw)
	}
	if !strings.Contains(string(raw), `"output_text"`) {
		t.Fatalf("assistant 历史文本应为 output_text 部件: %s", raw)
	}

	for name, msg := range map[string]string{
		"audio 块": `{"role":"user","content":[{"type":"input_audio","input_audio":{}}]}`,
		"图片缺 url": `{"role":"user","content":[{"type":"image_url","image_url":{}}]}`,
	} {
		raw := `{"model":"m","messages":[` + msg + `]}`
		if _, err := ChatToResponsesBody(mustChatReq(t, raw)); err == nil {
			t.Errorf("%s 应拒绝", name)
		}
	}
	badTool := mustChatReq(t, `{"model":"m","messages":[{"role":"user","content":"hi"}],
		"tools":[{"type":"web_search","function":{}}]}`)
	if _, err := ChatToResponsesBody(badTool); err == nil {
		t.Error("非 function 工具类型应拒绝")
	}
}

// TestChatRespToolChoice 断言 tool_choice 各形态映射。
func TestChatRespToolChoice(t *testing.T) {
	cases := []struct {
		name, raw string
		want      any
	}{
		{"auto", `"auto"`, "auto"},
		{"none", `"none"`, "none"},
		{"required", `"required"`, "required"},
		{"具名 function", `{"type":"function","function":{"name":"f1"}}`,
			map[string]string{"type": "function", "name": "f1"}},
		{"未知 string", `"whatever"`, nil},
		{"缺 name", `{"type":"function","function":{}}`, nil},
		{"空", ``, nil},
	}
	for _, tc := range cases {
		got := chatRespToolChoice(json.RawMessage(tc.raw))
		if gotMap, ok := got.(map[string]string); ok {
			wantMap, _ := tc.want.(map[string]string)
			if wantMap == nil || gotMap["name"] != wantMap["name"] {
				t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
			}
			continue
		}
		if got != tc.want {
			t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
		}
	}
}

// TestChatTextFormat 断言 response_format 拍平映射。
func TestChatTextFormat(t *testing.T) {
	if got := chatTextFormat(nil); got != nil {
		t.Errorf("nil 应不下发: %v", got)
	}
	if got := chatTextFormat(&aiwire.ResponseFormat{Type: "text"}); got != nil {
		t.Errorf("text 应不下发: %v", got)
	}
	if got := chatTextFormat(&aiwire.ResponseFormat{Type: "json_object"}); got["type"] != "json_object" {
		t.Errorf("json_object: %v", got)
	}
	got := chatTextFormat(&aiwire.ResponseFormat{Type: "json_schema",
		JSONSchema: json.RawMessage(`{"name":"out","schema":{"type":"object"},"strict":true}`)})
	if got["type"] != "json_schema" || got["name"] != "out" || got["strict"] != true {
		t.Errorf("json_schema 应提升嵌套字段: %v", got)
	}
	if _, ok := got["schema"]; !ok {
		t.Errorf("schema 缺失: %v", got)
	}
}

// TestResponsesToChat 断言文本聚合、tool_calls、finish_reason 与 usage 回转。
func TestResponsesToChat(t *testing.T) {
	payload := []byte(`{"model":"xai.grok-4.3","status":"completed","output":[
		{"type":"reasoning","summary":[]},
		{"type":"message","content":[{"type":"output_text","text":"你"},{"type":"output_text","text":"好"}]},
		{"type":"function_call","call_id":"c1","name":"get_weather","arguments":"{\"city\":\"东京\"}"}],
		"usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15,"input_tokens_details":{"cached_tokens":4}}}`)
	out, err := ResponsesToChat(payload, "chatcmpl-1", 1700000000)
	if err != nil {
		t.Fatalf("ResponsesToChat: %v", err)
	}
	if out.Object != "chat.completion" || out.ID != "chatcmpl-1" || out.Created != 1700000000 {
		t.Fatalf("响应骨架错误: %+v", out)
	}
	msg := out.Choices[0].Message
	if msg.Role != "assistant" || msg.Content.JoinText() != "你好" {
		t.Fatalf("content 装配错误: %+v", msg)
	}
	if len(msg.ToolCalls) != 1 || msg.ToolCalls[0].ID != "c1" ||
		msg.ToolCalls[0].Function.Arguments != `{"city":"东京"}` {
		t.Fatalf("tool_calls 装配错误: %+v", msg.ToolCalls)
	}
	if out.Choices[0].FinishReason != "tool_calls" {
		t.Fatalf("finish_reason = %s, want tool_calls", out.Choices[0].FinishReason)
	}
	if out.Usage.PromptTokens != 10 || out.Usage.CompletionTokens != 5 || out.Usage.CachedTokens() != 4 {
		t.Fatalf("usage = %+v", out.Usage)
	}

	trunc := []byte(`{"model":"m","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},
		"output":[{"type":"message","content":[{"type":"output_text","text":"半"}]}]}`)
	out2, err := ResponsesToChat(trunc, "chatcmpl-2", 1)
	if err != nil || out2.Choices[0].FinishReason != "length" {
		t.Fatalf("截断 finish_reason = %+v, %v", out2.Choices, err)
	}
	if out2.Usage != nil {
		t.Fatalf("无 usage 时应为 nil: %+v", out2.Usage)
	}
}

func chatChunkShapes(chunks []aiwire.ChatChunk) string {
	kinds := make([]string, 0, len(chunks))
	for _, ch := range chunks {
		switch {
		case len(ch.Choices) == 0:
			kinds = append(kinds, "usage")
		case ch.Choices[0].FinishReason != nil:
			kinds = append(kinds, "finish:"+*ch.Choices[0].FinishReason)
		case len(ch.Choices[0].Delta.ToolCalls) > 0:
			kinds = append(kinds, "tool")
		case ch.Choices[0].Delta.Role != "":
			kinds = append(kinds, "role+text")
		default:
			kinds = append(kinds, "text")
		}
	}
	return strings.Join(kinds, ",")
}

// TestChatRespBridge 断言流桥:首块 role、文本与工具增量、reasoning 丢弃、usage 块。
func TestChatRespBridge(t *testing.T) {
	b := NewChatRespBridge("chatcmpl-1", "m1", 1700000000, true)
	var chunks []aiwire.ChatChunk
	feed := func(lines ...string) {
		for _, l := range lines {
			chunks = append(chunks, b.Feed([]byte(l))...)
		}
	}
	feed(`{"type":"response.created","response":{"model":"m1"}}`,
		`{"type":"response.reasoning_text.delta","delta":"思考中"}`,
		`{"type":"response.output_text.delta","delta":"你"}`,
		`{"type":"response.output_text.delta","delta":"好"}`,
		`{"type":"response.output_item.added","item":{"type":"function_call","call_id":"c1","name":"f"}}`,
		`{"type":"response.function_call_arguments.delta","delta":"{\"a\":"}`,
		`{"type":"response.function_call_arguments.delta","delta":"1}"}`,
		`{"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":8,"output_tokens":4,"total_tokens":12}}}`)
	chunks = append(chunks, b.Finish()...)

	got := chatChunkShapes(chunks)
	want := "role+text,text,tool,tool,tool,finish:tool_calls,usage"
	if got != want {
		t.Fatalf("chunk 序列:\n got %s\nwant %s", got, want)
	}
	if chunks[0].Choices[0].Delta.Role != "assistant" || chunks[0].Choices[0].Delta.Content != "你" {
		t.Fatalf("首块应含 role 与文本: %+v", chunks[0].Choices[0].Delta)
	}
	tool := chunks[2].Choices[0].Delta.ToolCalls[0]
	if tool.Index != 0 || tool.ID != "c1" || tool.Function.Name != "f" {
		t.Fatalf("工具首块 = %+v", tool)
	}
	args := chunks[3].Choices[0].Delta.ToolCalls[0].Function.Arguments +
		chunks[4].Choices[0].Delta.ToolCalls[0].Function.Arguments
	if args != `{"a":1}` {
		t.Fatalf("实参增量聚合 = %s", args)
	}
	last := chunks[len(chunks)-1]
	if last.Usage == nil || last.Usage.PromptTokens != 8 || len(last.Choices) != 0 {
		t.Fatalf("usage 块 = %+v", last)
	}
	if b.Usage().TotalTokens != 12 {
		t.Fatalf("Usage() = %+v", b.Usage())
	}
}

// TestChatRespBridgeVariants 断言 include_usage 关闭、截断与空流的收尾形态。
func TestChatRespBridgeVariants(t *testing.T) {
	noUsage := NewChatRespBridge("c", "m", 1, false)
	noUsage.Feed([]byte(`{"type":"response.output_text.delta","delta":"x"}`))
	noUsage.Feed([]byte(`{"type":"response.incomplete","response":{"status":"incomplete","incomplete_details":{"reason":"max_output_tokens"}}}`))
	if got := chatChunkShapes(noUsage.Finish()); got != "finish:length" {
		t.Errorf("截断且不带 usage 块: %s", got)
	}

	empty := NewChatRespBridge("c", "m", 1, false)
	fin := empty.Finish()
	if got := chatChunkShapes(fin); got != "finish:stop" {
		t.Errorf("空流收尾 = %s", got)
	}
	if fin[0].Choices[0].Delta.Role != "assistant" {
		t.Errorf("空流终块应补 role: %+v", fin[0].Choices[0].Delta)
	}
}
