package providers

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

// staticTokenSource implements TokenSource for testing.
type staticTokenSource struct {
	token string
}

func (s *staticTokenSource) Token() (string, error) {
	return s.token, nil
}

// mustJSON marshals v to JSON string; panics on error.
func mustJSON(v any) string {
	b, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return string(b)
}

// writeSSEDone writes a minimal SSE response with just a completed event and [DONE].
func writeSSEDone(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "text/event-stream")
	fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{
		Type:     "response.completed",
		Response: &codexAPIResponse{ID: "resp-1", Status: "completed"},
	}))
	fmt.Fprint(w, "data: [DONE]\n\n")
}

func TestCodexProviderName(t *testing.T) {
	p := NewCodexProvider("openai-codex", &staticTokenSource{token: "test"}, "", "gpt-4o")
	if p.Name() != "openai-codex" {
		t.Errorf("Name() = %q, want %q", p.Name(), "openai-codex")
	}
}

func TestCodexProviderDefaultModel(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "")
	if p.DefaultModel() != "gpt-5.3-codex" {
		t.Errorf("DefaultModel() = %q, want %q", p.DefaultModel(), "gpt-5.3-codex")
	}

	p2 := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "o3")
	if p2.DefaultModel() != "o3" {
		t.Errorf("DefaultModel() = %q, want %q", p2.DefaultModel(), "o3")
	}
}

func TestCodexProviderBuildRequestBody(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Model: "gpt-4o",
		Messages: []Message{
			{Role: "system", Content: "You are helpful."},
			{Role: "user", Content: "Hello"},
			{Role: "assistant", Content: "Hi there!"},
			{Role: "user", Content: "How are you?"},
		},
		Options: map[string]any{
			OptMaxTokens:   1024,
			OptTemperature: 0.7,
		},
	}

	body := p.buildRequestBody(req, false)

	// Check model
	if body["model"] != "gpt-4o" {
		t.Errorf("model = %v, want gpt-4o", body["model"])
	}

	// Check stream
	if body["stream"] != false {
		t.Errorf("stream = %v, want false", body["stream"])
	}

	// Check store is false
	if body["store"] != false {
		t.Errorf("store = %v, want false", body["store"])
	}

	// Check instructions extracted from system message
	if body["instructions"] != "You are helpful." {
		t.Errorf("instructions = %v, want 'You are helpful.'", body["instructions"])
	}

	// Check input items (should exclude system messages)
	input, ok := body["input"].([]any)
	if !ok {
		t.Fatalf("input is not []interface{}: %T", body["input"])
	}
	if len(input) != 3 {
		t.Fatalf("input length = %d, want 3 (user + assistant + user)", len(input))
	}

	// chatgpt.com backend does not support max_output_tokens or temperature
	if _, ok := body["max_output_tokens"]; ok {
		t.Error("max_output_tokens should not be set (unsupported by chatgpt.com backend)")
	}
	if _, ok := body["temperature"]; ok {
		t.Error("temperature should not be set (unsupported by chatgpt.com backend)")
	}
}

func TestCodexProviderBuildRequestBodyWithTools(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Messages: []Message{{Role: "user", Content: "What's the weather?"}},
		Tools: []ToolDefinition{
			{
				Function: ToolFunctionSchema{
					Name:        "get_weather",
					Description: "Get current weather",
					Parameters: map[string]any{
						"type": "object",
						"properties": map[string]any{
							"city": map[string]any{"type": "string"},
						},
						"required": []string{"city"},
					},
				},
			},
		},
	}

	body := p.buildRequestBody(req, true)

	tools, ok := body["tools"].([]map[string]any)
	if !ok {
		t.Fatalf("tools is not []map[string]interface{}: %T", body["tools"])
	}
	if len(tools) != 1 {
		t.Fatalf("tools length = %d, want 1", len(tools))
	}

	tool := tools[0]
	if tool["type"] != "function" {
		t.Errorf("tool type = %v, want function", tool["type"])
	}
	if tool["name"] != "get_weather" {
		t.Errorf("tool name = %v, want get_weather", tool["name"])
	}
	if _, hasStrict := tool["strict"]; hasStrict {
		t.Error("tool should not have strict field")
	}
}

func TestCodexProviderBuildRequestBodyToolCallMessages(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Messages: []Message{
			{Role: "user", Content: "What's the weather?"},
			{
				Role: "assistant",
				ToolCalls: []ToolCall{
					{ID: "call_123", Name: "get_weather", Arguments: map[string]any{"city": "London"}},
				},
			},
			{Role: "tool", ToolCallID: "call_123", Content: `{"temp": 15}`},
		},
	}

	body := p.buildRequestBody(req, false)

	input, ok := body["input"].([]any)
	if !ok {
		t.Fatalf("input is not []interface{}: %T", body["input"])
	}
	if len(input) != 3 {
		t.Fatalf("input length = %d, want 3", len(input))
	}

	// Second item should be function_call
	fc, ok := input[1].(map[string]any)
	if !ok {
		t.Fatalf("input[1] is not map: %T", input[1])
	}
	if fc["type"] != "function_call" {
		t.Errorf("input[1] type = %v, want function_call", fc["type"])
	}
	if fc["name"] != "get_weather" {
		t.Errorf("input[1] name = %v, want get_weather", fc["name"])
	}

	// Third item should be function_call_output
	fco, ok := input[2].(map[string]any)
	if !ok {
		t.Fatalf("input[2] is not map: %T", input[2])
	}
	if fco["type"] != "function_call_output" {
		t.Errorf("input[2] type = %v, want function_call_output", fco["type"])
	}
	if fco["call_id"] != "fc_123" {
		t.Errorf("input[2] call_id = %v, want fc_123", fco["call_id"])
	}
}

func TestCodexProviderBuildRequestBodyThinking(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Messages: []Message{{Role: "user", Content: "Think about this"}},
		Options:  map[string]any{OptThinkingLevel: "high"},
	}

	body := p.buildRequestBody(req, false)

	reasoning, ok := body["reasoning"].(map[string]any)
	if !ok {
		t.Fatalf("reasoning is not map: %T", body["reasoning"])
	}
	if reasoning["effort"] != "high" {
		t.Errorf("reasoning effort = %v, want high", reasoning["effort"])
	}
}

func TestCodexProviderChat(t *testing.T) {
	// Mock Responses API server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Verify headers
		if r.Header.Get("Authorization") != "Bearer test-token" {
			t.Errorf("Authorization = %q, want Bearer test-token", r.Header.Get("Authorization"))
		}
		if r.Header.Get("OpenAI-Beta") != "responses=v1" {
			t.Errorf("OpenAI-Beta = %q, want responses=v1", r.Header.Get("OpenAI-Beta"))
		}

		// Verify endpoint
		if r.URL.Path != "/codex/responses" {
			t.Errorf("path = %q, want /responses", r.URL.Path)
		}

		// Verify request body
		var body map[string]any
		json.NewDecoder(r.Body).Decode(&body)
		if body["model"] != "gpt-4o" {
			t.Errorf("request model = %v, want gpt-4o", body["model"])
		}
		if body["store"] != false {
			t.Errorf("request store = %v, want false", body["store"])
		}

		// Return SSE mock response (Chat delegates to ChatStream)
		w.Header().Set("Content-Type", "text/event-stream")
		fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{Type: "response.output_text.delta", Delta: "Hello! I'm doing great."}))
		fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{
			Type: "response.output_item.done",
			Item: &codexItem{Type: "message", Role: "assistant"},
		}))
		fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{
			Type: "response.completed",
			Response: &codexAPIResponse{
				ID: "resp-123", Status: "completed",
				Usage: &codexUsage{InputTokens: 10, OutputTokens: 8, TotalTokens: 18},
			},
		}))
		fmt.Fprint(w, "data: [DONE]\n\n")
	}))
	defer server.Close()

	p := NewCodexProvider("openai-codex", &staticTokenSource{token: "test-token"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	result, err := p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Hello"}},
	})
	if err != nil {
		t.Fatalf("Chat: %v", err)
	}

	if result.Content != "Hello! I'm doing great." {
		t.Errorf("Content = %q, want 'Hello! I'm doing great.'", result.Content)
	}
	if result.FinishReason != "stop" {
		t.Errorf("FinishReason = %q, want stop", result.FinishReason)
	}
	if result.Usage == nil {
		t.Fatal("Usage is nil")
	}
	if result.Usage.PromptTokens != 10 {
		t.Errorf("PromptTokens = %d, want 10", result.Usage.PromptTokens)
	}
}

func TestCodexProviderChatStream(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		flusher := w.(http.Flusher)

		events := []string{
			`{"type":"response.output_text.delta","delta":"Hello"}`,
			`{"type":"response.output_text.delta","delta":" world"}`,
			`{"type":"response.completed","response":{"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`,
		}

		for _, e := range events {
			fmt.Fprintf(w, "data: %s\n\n", e)
			flusher.Flush()
		}
		fmt.Fprint(w, "data: [DONE]\n\n")
		flusher.Flush()
	}))
	defer server.Close()

	p := NewCodexProvider("openai-codex", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	var chunks []string
	result, err := p.ChatStream(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Hi"}},
	}, func(chunk StreamChunk) {
		if chunk.Content != "" {
			chunks = append(chunks, chunk.Content)
		}
	})
	if err != nil {
		t.Fatalf("ChatStream: %v", err)
	}

	if result.Content != "Hello world" {
		t.Errorf("Content = %q, want 'Hello world'", result.Content)
	}
	if len(chunks) != 2 {
		t.Errorf("chunks = %v, want 2 chunks", chunks)
	}
	if result.Usage == nil {
		t.Fatal("Usage is nil")
	}
	if result.Usage.TotalTokens != 7 {
		t.Errorf("TotalTokens = %d, want 7", result.Usage.TotalTokens)
	}
}

func TestCodexProviderChatStreamToolCalls(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		flusher := w.(http.Flusher)

		events := []string{
			`{"type":"response.function_call_arguments.delta","item_id":"item_1","delta":"{\"city\""}`,
			`{"type":"response.function_call_arguments.delta","item_id":"item_1","delta":":\"London\"}"}`,
			`{"type":"response.output_item.done","item":{"type":"function_call","id":"item_1","call_id":"call_abc","name":"get_weather","arguments":"{\"city\":\"London\"}"}}`,
			`{"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15}}}`,
		}

		for _, e := range events {
			fmt.Fprintf(w, "data: %s\n\n", e)
			flusher.Flush()
		}
		fmt.Fprint(w, "data: [DONE]\n\n")
		flusher.Flush()
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	result, err := p.ChatStream(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Weather?"}},
	}, nil)
	if err != nil {
		t.Fatalf("ChatStream: %v", err)
	}

	if result.FinishReason != "tool_calls" {
		t.Errorf("FinishReason = %q, want tool_calls", result.FinishReason)
	}
	if len(result.ToolCalls) != 1 {
		t.Fatalf("ToolCalls length = %d, want 1", len(result.ToolCalls))
	}
	tc := result.ToolCalls[0]
	if tc.Name != "get_weather" {
		t.Errorf("ToolCall name = %q, want get_weather", tc.Name)
	}
	if tc.ID != "call_abc" {
		t.Errorf("ToolCall ID = %q, want call_abc", tc.ID)
	}
}

func TestCodexProviderChatToolCallsNonStream(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{
			Type: "response.output_item.done",
			Item: &codexItem{
				Type:      "function_call",
				ID:        "item_1",
				CallID:    "call_xyz",
				Name:      "search",
				Arguments: `{"query":"test"}`,
			},
		}))
		fmt.Fprintf(w, "data: %s\n\n", mustJSON(codexSSEEvent{
			Type: "response.completed",
			Response: &codexAPIResponse{
				ID: "resp-456", Status: "completed",
				Usage: &codexUsage{InputTokens: 5, OutputTokens: 3, TotalTokens: 8},
			},
		}))
		fmt.Fprint(w, "data: [DONE]\n\n")
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	result, err := p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Search"}},
	})
	if err != nil {
		t.Fatalf("Chat: %v", err)
	}

	if result.FinishReason != "tool_calls" {
		t.Errorf("FinishReason = %q, want tool_calls", result.FinishReason)
	}
	if len(result.ToolCalls) != 1 {
		t.Fatalf("ToolCalls = %d, want 1", len(result.ToolCalls))
	}
	if result.ToolCalls[0].Arguments["query"] != "test" {
		t.Errorf("ToolCall arg query = %v, want test", result.ToolCalls[0].Arguments["query"])
	}
}

func TestCodexProviderHTTPError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusTooManyRequests)
		w.Write([]byte(`{"error":"rate limited"}`))
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	_, err := p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Hi"}},
	})
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "rate limited") {
		t.Errorf("error = %q, expected to contain 'rate limited'", err.Error())
	}
}

func TestCodexProviderMultipleSystemMessages(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Messages: []Message{
			{Role: "system", Content: "First instruction."},
			{Role: "system", Content: "Second instruction."},
			{Role: "user", Content: "Hello"},
		},
	}

	body := p.buildRequestBody(req, false)

	expected := "First instruction.\n\nSecond instruction."
	if body["instructions"] != expected {
		t.Errorf("instructions = %q, want %q", body["instructions"], expected)
	}
}

func TestCodexProviderStreamReasoning(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		flusher := w.(http.Flusher)

		events := []string{
			`{"type":"response.output_item.done","item":{"type":"reasoning","summary":[{"type":"summary_text","text":"Thinking about this..."}]}}`,
			`{"type":"response.output_text.delta","delta":"The answer is 42."}`,
			`{"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"output_tokens_details":{"reasoning_tokens":15}}}}`,
		}

		for _, e := range events {
			fmt.Fprintf(w, "data: %s\n\n", e)
			flusher.Flush()
		}
		fmt.Fprint(w, "data: [DONE]\n\n")
		flusher.Flush()
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	var thinkingChunks []string
	result, err := p.ChatStream(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "Think"}},
	}, func(chunk StreamChunk) {
		if chunk.Thinking != "" {
			thinkingChunks = append(thinkingChunks, chunk.Thinking)
		}
	})
	if err != nil {
		t.Fatalf("ChatStream: %v", err)
	}

	if result.Thinking != "Thinking about this..." {
		t.Errorf("Thinking = %q, want 'Thinking about this...'", result.Thinking)
	}
	if result.Content != "The answer is 42." {
		t.Errorf("Content = %q, want 'The answer is 42.'", result.Content)
	}
	if result.Usage != nil && result.Usage.ThinkingTokens != 15 {
		t.Errorf("ThinkingTokens = %d, want 15", result.Usage.ThinkingTokens)
	}
}

// Verify the endpoint format (apiBase + /responses)
func TestCodexProviderEndpoint(t *testing.T) {
	var capturedPath string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		capturedPath = r.URL.Path
		writeSSEDone(w)
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1

	p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "test"}},
	})

	if capturedPath != "/codex/responses" {
		t.Errorf("endpoint path = %q, want /responses", capturedPath)
	}
}

// Verify trailing slash is stripped from apiBase
func TestCodexProviderAPIBaseTrailingSlash(t *testing.T) {
	var capturedPath string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		capturedPath = r.URL.Path
		writeSSEDone(w)
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, server.URL+"/", "gpt-4o")
	p.retryConfig.Attempts = 1
	p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "test"}},
	})

	if capturedPath != "/codex/responses" {
		t.Errorf("endpoint path = %q, want /responses (trailing slash should be stripped)", capturedPath)
	}
}

// Ensure token is used from TokenSource
func TestCodexProviderTokenSource(t *testing.T) {
	var capturedAuth string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		capturedAuth = r.Header.Get("Authorization")
		writeSSEDone(w)
	}))
	defer server.Close()

	p := NewCodexProvider("test", &staticTokenSource{token: "my-oauth-token"}, server.URL, "gpt-4o")
	p.retryConfig.Attempts = 1
	p.Chat(context.Background(), ChatRequest{
		Messages: []Message{{Role: "user", Content: "test"}},
	})

	if capturedAuth != "Bearer my-oauth-token" {
		t.Errorf("Authorization = %q, want 'Bearer my-oauth-token'", capturedAuth)
	}
}

// Verify request body includes image content
func TestCodexProviderBuildRequestBodyWithImages(t *testing.T) {
	p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "gpt-4o")

	req := ChatRequest{
		Messages: []Message{
			{
				Role:    "user",
				Content: "What's this?",
				Images:  []ImageContent{{MimeType: "image/png", Data: "base64data"}},
			},
		},
	}

	body := p.buildRequestBody(req, false)

	input, ok := body["input"].([]any)
	if !ok || len(input) != 1 {
		t.Fatalf("expected 1 input item, got %v", body["input"])
	}

	item, ok := input[0].(map[string]any)
	if !ok {
		t.Fatalf("input[0] is not map: %T", input[0])
	}

	content, ok := item["content"].([]map[string]any)
	if !ok {
		t.Fatalf("content is not []map: %T", item["content"])
	}

	// Should have image + text
	if len(content) != 2 {
		t.Fatalf("content length = %d, want 2 (image + text)", len(content))
	}
	if content[0]["type"] != "input_image" {
		t.Errorf("content[0] type = %v, want input_image", content[0]["type"])
	}
	if content[1]["type"] != "input_text" {
		t.Errorf("content[1] type = %v, want input_text", content[1]["type"])
	}
}
