package provider

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"GoNavi-Wails/internal/ai"
)

func TestNormalizeOpenAICompatibleBaseURL(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		want string
	}{
		{
			name: "empty uses default openai base url",
			raw:  "",
			want: "https://api.openai.com/v1",
		},
		{
			name: "domain only appends v1",
			raw:  "https://api.openai.com",
			want: "https://api.openai.com/v1",
		},
		{
			name: "keeps existing v1 suffix",
			raw:  "https://api.deepseek.com/v1",
			want: "https://api.deepseek.com/v1",
		},
		{
			name: "keeps dashscope compatible mode path",
			raw:  "https://dashscope.aliyuncs.com/compatible-mode/v1",
			want: "https://dashscope.aliyuncs.com/compatible-mode/v1",
		},
		{
			name: "keeps zhipu v4 path",
			raw:  "https://open.bigmodel.cn/api/paas/v4",
			want: "https://open.bigmodel.cn/api/paas/v4",
		},
		{
			name: "keeps volcengine ark v3 path",
			raw:  "https://ark.cn-beijing.volces.com/api/v3",
			want: "https://ark.cn-beijing.volces.com/api/v3",
		},
		{
			name: "keeps volcengine coding plan v3 path",
			raw:  "https://ark.cn-beijing.volces.com/api/coding/v3",
			want: "https://ark.cn-beijing.volces.com/api/coding/v3",
		},
		{
			name: "strips chat completions suffix before normalizing",
			raw:  "https://api.openai.com/v1/chat/completions",
			want: "https://api.openai.com/v1",
		},
		{
			name: "strips models suffix before normalizing",
			raw:  "https://ark.cn-beijing.volces.com/api/coding/v3/models",
			want: "https://ark.cn-beijing.volces.com/api/coding/v3",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := NormalizeOpenAICompatibleBaseURL(tt.raw); got != tt.want {
				t.Fatalf("expected normalized base url %q, got %q", tt.want, got)
			}
		})
	}
}

func TestResolveOpenAICompatibleEndpoint(t *testing.T) {
	got := ResolveOpenAICompatibleEndpoint("https://ark.cn-beijing.volces.com/api/coding/v3/models", "chat/completions")
	want := "https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions"
	if got != want {
		t.Fatalf("expected endpoint %q, got %q", want, got)
	}
}

func TestOpenAIProvider_Validate_MissingAPIKey(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{Type: "openai", Model: "gpt-4o"})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	if err := p.Validate(); err == nil {
		t.Fatal("expected validation error for missing API key")
	}
}

func TestOpenAIProvider_Validate_Valid(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test-key", Model: "gpt-4o",
	})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	if err := p.Validate(); err != nil {
		t.Fatalf("unexpected validation error: %v", err)
	}
}

func TestOpenAIProvider_Name_Custom(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", Name: "My OpenAI", APIKey: "sk-test", Model: "gpt-4o",
	})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	if p.Name() != "My OpenAI" {
		t.Fatalf("expected name 'My OpenAI', got '%s'", p.Name())
	}
}

func TestOpenAIProvider_Name_Default(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test", Model: "gpt-4o",
	})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	if p.Name() != "OpenAI" {
		t.Fatalf("expected default name 'OpenAI', got '%s'", p.Name())
	}
}

func TestOpenAIProvider_DefaultBaseURL(t *testing.T) {
	p, _ := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test", Model: "gpt-4o",
	})
	op := p.(*OpenAIProvider)
	if op.baseURL != "https://api.openai.com/v1" {
		t.Fatalf("expected default base URL, got '%s'", op.baseURL)
	}
}

func TestOpenAIProvider_CustomBaseURL(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test", BaseURL: "https://my-proxy.com/v1", Model: "gpt-4o",
	})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	op := p.(*OpenAIProvider)
	if op.baseURL != "https://my-proxy.com/v1" {
		t.Fatalf("expected custom base URL, got '%s'", op.baseURL)
	}
}

func TestOpenAIProvider_RejectsMissingModel(t *testing.T) {
	_, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test",
	})
	if err == nil {
		t.Fatal("expected constructor error for missing model")
	}
}

func TestOpenAIProvider_DefaultMaxTokens(t *testing.T) {
	p, err := NewOpenAIProvider(ai.ProviderConfig{
		Type: "openai", APIKey: "sk-test", Model: "gpt-4o",
	})
	if err != nil {
		t.Fatalf("unexpected constructor error: %v", err)
	}
	op := p.(*OpenAIProvider)
	if op.config.MaxTokens != 4096 {
		t.Fatalf("expected default max tokens 4096, got %d", op.config.MaxTokens)
	}
}

func TestOpenAIProviderChatRetriesWithoutImagesOnHTTP400(t *testing.T) {
	requestCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestCount++
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("read request body failed: %v", err)
		}
		defer r.Body.Close()

		if strings.Contains(string(body), `"image_url"`) {
			http.Error(w, `{"error":{"message":"Model do not support image input"}}`, http.StatusBadRequest)
			return
		}
		if !strings.Contains(string(body), omittedImageNotice) {
			t.Fatalf("expected retry body to explain omitted image, got %s", body)
		}

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"pong"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`))
	}))
	defer server.Close()

	providerInstance, err := NewOpenAIProvider(ai.ProviderConfig{
		Type:        "openai",
		Name:        "test-openai",
		APIKey:      "sk-test",
		BaseURL:     server.URL,
		Model:       "custom-text-model",
		MaxTokens:   64,
		Temperature: 0.1,
	})
	if err != nil {
		t.Fatalf("create provider failed: %v", err)
	}

	resp, err := providerInstance.Chat(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{
			Role:    "user",
			Content: "请描述这张图片",
			Images:  []string{"data:image/png;base64,abc"},
		}},
	})
	if err != nil {
		t.Fatalf("expected chat image fallback to succeed, got %v", err)
	}
	if resp.Content != "pong" {
		t.Fatalf("expected fallback content %q, got %q", "pong", resp.Content)
	}
	if requestCount != 2 {
		t.Fatalf("expected 2 requests (with image then fallback), got %d", requestCount)
	}
}

func TestOpenAIProviderChatOmitsImagesUpfrontForMiniMaxTextModel(t *testing.T) {
	requestCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestCount++
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("read request body failed: %v", err)
		}
		defer r.Body.Close()

		bodyText := string(body)
		if strings.Contains(bodyText, `"image_url"`) {
			t.Fatalf("expected MiniMax text request to omit image_url, got %s", body)
		}
		if !strings.Contains(bodyText, omittedImageNotice) {
			t.Fatalf("expected request body to explain omitted image, got %s", body)
		}

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"pong"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`))
	}))
	defer server.Close()

	providerInstance, err := NewOpenAIProvider(ai.ProviderConfig{
		Type:        "openai",
		Name:        "test-openai",
		APIKey:      "sk-test",
		BaseURL:     server.URL,
		Model:       "MiniMax-M2.7-highspeed",
		MaxTokens:   64,
		Temperature: 0.1,
	})
	if err != nil {
		t.Fatalf("create provider failed: %v", err)
	}

	resp, err := providerInstance.Chat(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{
			Role:    "user",
			Content: "请描述这张图片",
			Images:  []string{"data:image/png;base64,abc"},
		}},
	})
	if err != nil {
		t.Fatalf("expected chat to succeed without sending image, got %v", err)
	}
	if resp.Content != "pong" {
		t.Fatalf("expected content %q, got %q", "pong", resp.Content)
	}
	if requestCount != 1 {
		t.Fatalf("expected 1 request without image retry, got %d", requestCount)
	}
}

func TestPrepareOpenAIRequestMessagesKeepsImagesForVisionModel(t *testing.T) {
	got := prepareOpenAIRequestMessages([]ai.Message{{
		Role:    "user",
		Content: "请描述图片",
		Images:  []string{"data:image/png;base64,abc"},
	}}, "gpt-5.4", "https://sub.syngnat.top/v1")

	if len(got) != 1 || len(got[0].Images) != 1 {
		t.Fatalf("expected vision-capable model to keep images, got %#v", got)
	}
}

func TestOpenAIProviderChatStreamRetriesWithoutToolsThenImagesOnHTTP400(t *testing.T) {
	requestCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestCount++
		body, err := io.ReadAll(r.Body)
		if err != nil {
			t.Fatalf("read request body failed: %v", err)
		}
		defer r.Body.Close()

		bodyText := string(body)
		if strings.Contains(bodyText, `"tools"`) {
			http.Error(w, `{"error":{"message":"A parameter specified in the request is not valid"}}`, http.StatusBadRequest)
			return
		}
		if strings.Contains(bodyText, `"image_url"`) {
			http.Error(w, `{"error":{"message":"A parameter specified in the request is not valid"}}`, http.StatusBadRequest)
			return
		}
		if !strings.Contains(bodyText, omittedImageNotice) {
			t.Fatalf("expected retry body to explain omitted image, got %s", body)
		}

		w.Header().Set("Content-Type", "text/event-stream")
		_, _ = w.Write([]byte(strings.Join([]string{
			`data: {"choices":[{"delta":{"content":"pong"},"finish_reason":null}]}`,
			``,
			`data: [DONE]`,
			``,
		}, "\n")))
	}))
	defer server.Close()

	providerInstance, err := NewOpenAIProvider(ai.ProviderConfig{
		Type:        "openai",
		Name:        "test-openai",
		APIKey:      "sk-test",
		BaseURL:     server.URL,
		Model:       "custom-text-model",
		MaxTokens:   64,
		Temperature: 0.1,
	})
	if err != nil {
		t.Fatalf("create provider failed: %v", err)
	}

	var chunks []ai.StreamChunk
	err = providerInstance.ChatStream(context.Background(), ai.ChatRequest{
		Messages: []ai.Message{{
			Role:    "user",
			Content: "请描述这张图片",
			Images:  []string{"data:image/png;base64,abc"},
		}},
		Tools: []ai.Tool{{
			Type: "function",
			Function: ai.ToolFunction{
				Name:        "inspect_ai_last_render_error",
				Description: "test tool",
				Parameters:  map[string]interface{}{"type": "object"},
			},
		}},
	}, func(chunk ai.StreamChunk) {
		chunks = append(chunks, chunk)
	})
	if err != nil {
		t.Fatalf("expected stream fallback to succeed, got %v", err)
	}
	if requestCount != 3 {
		t.Fatalf("expected 3 requests (with tools, without tools, without images), got %d", requestCount)
	}
	if len(chunks) < 2 {
		t.Fatalf("expected content and done chunks, got %#v", chunks)
	}
	if chunks[0].Content != "pong" {
		t.Fatalf("expected first chunk content %q, got %#v", "pong", chunks[0])
	}
	if !chunks[len(chunks)-1].Done {
		t.Fatalf("expected final done chunk, got %#v", chunks[len(chunks)-1])
	}
}

func TestBuildOpenAIMessages_ReplaysDeepSeekReasoningContentForToolCalls(t *testing.T) {
	toolCall := testOpenAIToolCall()
	got := buildOpenAIMessages([]ai.Message{
		{
			Role:             "assistant",
			Content:          "",
			ToolCalls:        []ai.ToolCall{toolCall},
			ReasoningContent: "需要先检查表结构",
		},
		{
			Role:       "tool",
			Content:    `{"ok":true}`,
			ToolCallID: toolCall.ID,
		},
	}, "deepseek-v4", "https://api.deepseek.com/v1")

	if got[0].ReasoningContent != "需要先检查表结构" {
		t.Fatalf("expected reasoning_content to be replayed for DeepSeek tool call, got %q", got[0].ReasoningContent)
	}
	if got[1].ReasoningContent != "" {
		t.Fatalf("expected tool result message not to carry reasoning_content, got %q", got[1].ReasoningContent)
	}

	body, err := json.Marshal(got[0])
	if err != nil {
		t.Fatalf("marshal message: %v", err)
	}
	if !strings.Contains(string(body), `"reasoning_content":"需要先检查表结构"`) {
		t.Fatalf("expected JSON payload to include reasoning_content, got %s", body)
	}
}

func TestBuildOpenAIMessages_OmitsReasoningContentForNonDeepSeekProviders(t *testing.T) {
	got := buildOpenAIMessages([]ai.Message{
		{
			Role:             "assistant",
			Content:          "",
			ToolCalls:        []ai.ToolCall{testOpenAIToolCall()},
			ReasoningContent: "reasoning should stay local",
		},
	}, "gpt-4o", "https://api.openai.com/v1")

	if got[0].ReasoningContent != "" {
		t.Fatalf("expected non-DeepSeek provider to omit reasoning_content, got %q", got[0].ReasoningContent)
	}
	body, err := json.Marshal(got[0])
	if err != nil {
		t.Fatalf("marshal message: %v", err)
	}
	if strings.Contains(string(body), "reasoning_content") {
		t.Fatalf("expected JSON payload to omit reasoning_content for non-DeepSeek provider, got %s", body)
	}
}

func TestBuildOpenAIMessages_ReplaysDeepSeekAssistantReasoningContentWithoutToolCalls(t *testing.T) {
	got := buildOpenAIMessages([]ai.Message{
		{
			Role:             "assistant",
			Content:          "最终分析",
			ReasoningContent: "工具调用轮次的最终思考也需要保留",
		},
	}, "deepseek-v4", "https://api.deepseek.com/v1")

	if got[0].ReasoningContent != "工具调用轮次的最终思考也需要保留" {
		t.Fatalf("expected DeepSeek assistant reasoning_content to be replayed, got %q", got[0].ReasoningContent)
	}
}

func testOpenAIToolCall() ai.ToolCall {
	var toolCall ai.ToolCall
	toolCall.ID = "call_schema"
	toolCall.Type = "function"
	toolCall.Function.Name = "inspect_table_schema"
	toolCall.Function.Arguments = `{"table":"orders"}`
	return toolCall
}
