package clients

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"testing"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func correctionTestRequest(server *httptest.Server, body map[string]any) Request {
	return Request{
		Kind:  "chat.completions",
		Model: "Qwen3.8-Max-Preview",
		Body:  body,
		Candidate: store.RuntimeModelCandidate{
			Provider:          "aliyun-bailian-openai",
			BaseURL:           server.URL,
			ProviderModelName: "qwen3.8-max-preview",
			Credentials:       map[string]any{"apiKey": "test-key"},
		},
	}
}

func writeCorrectionSuccess(w http.ResponseWriter, model any) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"id": "chatcmpl-corrected", "object": "chat.completion", "model": model,
		"choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "ok"}}},
		"usage":   map[string]any{"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
	})
}

func writeParameterError(w http.ResponseWriter, param string, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusBadRequest)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"error": map[string]any{"code": "invalid_parameter", "param": param, "message": message},
	})
}

func TestOpenAIParameterCorrectionLearnsOnlyAfterSuccess(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["n"] != float64(1) {
			writeParameterError(w, "n", "Parameter n must be set to 1.")
			return
		}
		writeCorrectionSuccess(w, body["model"])
	}))
	defer server.Close()

	cache := NewParameterCorrectionCache()
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: cache}
	request := correctionTestRequest(server, map[string]any{
		"messages": []any{map[string]any{"role": "user", "content": "hello"}},
		"n":        2,
	})
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("first corrected request failed: %v", err)
	}
	if got := requests.Load(); got != 2 {
		t.Fatalf("first logical request should send twice, got %d", got)
	}
	if cache.size() != 1 {
		t.Fatalf("successful correction should be cached, size=%d", cache.size())
	}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("cached request failed: %v", err)
	}
	if got := requests.Load(); got != 3 {
		t.Fatalf("second logical request should hit cache on first send, got total %d", got)
	}
}

func TestOpenAIParameterCorrectionLearnsDashScopeThinkingNConstraint(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["n"] != float64(1) {
			writeParameterError(w, "n", "The n parameter must be 1 when enable_thinking is true")
			return
		}
		writeCorrectionSuccess(w, body["model"])
	}))
	defer server.Close()
	cache := NewParameterCorrectionCache()
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: cache}
	_, err := client.Run(context.Background(), correctionTestRequest(server, map[string]any{
		"messages": []any{}, "n": 2,
	}))
	if err != nil {
		t.Fatalf("DashScope n constraint should be corrected: %v", err)
	}
	if cache.size() != 1 {
		t.Fatalf("successful DashScope n correction should be cached, size=%d", cache.size())
	}
}

func TestOpenAIParameterCorrectionFailureIsNotCached(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		writeParameterError(w, "n", "Parameter n must be set to 1.")
	}))
	defer server.Close()
	cache := NewParameterCorrectionCache()
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: cache}
	_, err := client.Run(context.Background(), correctionTestRequest(server, map[string]any{
		"messages": []any{}, "n": 2,
	}))
	if err == nil {
		t.Fatal("expected upstream error")
	}
	if cache.size() != 0 {
		t.Fatalf("failed correction must not be cached, size=%d", cache.size())
	}
}

func TestOpenAIParameterCorrectionStopsAfterTwoRetries(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["n"] != float64(1) {
			writeParameterError(w, "n", "Parameter n must be set to 1.")
			return
		}
		if body["temperature"] != float64(1) {
			writeParameterError(w, "temperature", "Parameter temperature must be set to 1.")
			return
		}
		writeParameterError(w, "top_p", "Parameter top_p must be set to 1.")
	}))
	defer server.Close()
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}
	_, err := client.Run(context.Background(), correctionTestRequest(server, map[string]any{
		"messages": []any{}, "n": 2, "temperature": 2, "top_p": 2,
	}))
	if err == nil {
		t.Fatal("expected third parameter error")
	}
	if got := requests.Load(); got != 3 {
		t.Fatalf("expected initial request plus two correction retries, got %d", got)
	}
}

func TestOpenAIParameterCorrectionEvictsConflictingRule(t *testing.T) {
	cache := NewParameterCorrectionCache()
	var seen []float64
	var mu sync.Mutex
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		n, _ := body["n"].(float64)
		mu.Lock()
		seen = append(seen, n)
		mu.Unlock()
		if n != 2 {
			writeParameterError(w, "n", "Parameter n must be set to 2.")
			return
		}
		writeCorrectionSuccess(w, body["model"])
	}))
	defer server.Close()
	request := correctionTestRequest(server, map[string]any{"messages": []any{}, "n": 9})
	scope := newParameterCorrectionScope(request, "chat.completions")
	cache.commit(scope, []parameterCorrectionRule{{Param: "n", Action: parameterCorrectionSet, Value: 1}})
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: cache}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("conflicting cache correction failed: %v", err)
	}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("relearned cache request failed: %v", err)
	}
	mu.Lock()
	defer mu.Unlock()
	if fmt.Sprint(seen) != "[1 2 2]" {
		t.Fatalf("expected old rule, relearn retry, then new cache hit; got %v", seen)
	}
}

func TestOpenAIParameterCorrectionNeverTouchesDangerousFields(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		requests.Add(1)
		writeParameterError(w, "model", "Unsupported parameter: model")
	}))
	defer server.Close()
	messages := []any{map[string]any{"role": "user", "content": "secret text"}}
	request := correctionTestRequest(server, map[string]any{"messages": messages})
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}
	_, err := client.Run(context.Background(), request)
	if err == nil {
		t.Fatal("expected unsafe parameter error")
	}
	if requests.Load() != 1 {
		t.Fatalf("unsafe parameter must not retry, got %d requests", requests.Load())
	}
	if fmt.Sprint(request.Body["messages"]) != fmt.Sprint(messages) {
		t.Fatal("messages were unexpectedly modified")
	}
}

func TestOpenAIParameterCorrectionWorksBeforeStreamingOutput(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["n"] != float64(1) {
			writeParameterError(w, "n", "Parameter n must be set to 1.")
			return
		}
		w.Header().Set("Content-Type", "text/event-stream")
		_, _ = fmt.Fprint(w, "data: {\"id\":\"chatcmpl-stream\",\"choices\":[{\"delta\":{\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")
	}))
	defer server.Close()
	var deltas []string
	request := correctionTestRequest(server, map[string]any{"messages": []any{}, "n": 2, "stream": true})
	request.Stream = true
	request.StreamDelta = func(event StreamDeltaEvent) error {
		deltas = append(deltas, event.Text)
		return nil
	}
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("stream correction failed: %v", err)
	}
	if requests.Load() != 2 || fmt.Sprint(deltas) != "[ok]" {
		t.Fatalf("unexpected stream correction requests=%d deltas=%v", requests.Load(), deltas)
	}
}

func TestOpenAIResponsesParameterCorrectionLearnsAfterSuccess(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["top_p"] != float64(1) {
			writeParameterError(w, "top_p", "Parameter top_p must be set to 1.")
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id": "resp-corrected", "object": "response", "status": "completed",
			"output": []any{}, "usage": map[string]any{"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
		})
	}))
	defer server.Close()
	cache := NewParameterCorrectionCache()
	client := OpenAIClient{HTTPClient: server.Client(), Corrections: cache}
	request := Request{
		Kind:             "responses",
		Model:            "Qwen3.8-Max-Preview",
		UpstreamProtocol: ProtocolOpenAIResponses,
		Body:             map[string]any{"input": "hello", "top_p": 2, "reasoning": map[string]any{"effort": "none"}},
		Candidate: store.RuntimeModelCandidate{
			Provider: "aliyun-bailian-openai", BaseURL: server.URL,
			ProviderModelName: "qwen3.8-max-preview", Credentials: map[string]any{"apiKey": "test-key"},
		},
	}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("first corrected Responses request failed: %v", err)
	}
	if _, err := client.Run(context.Background(), request); err != nil {
		t.Fatalf("cached Responses request failed: %v", err)
	}
	if requests.Load() != 3 || cache.size() != 1 {
		t.Fatalf("expected retry then Responses cache hit, requests=%d cache=%d", requests.Load(), cache.size())
	}
}

func TestParameterCorrectionCacheIsConcurrentAndBounded(t *testing.T) {
	cache := NewParameterCorrectionCache()
	var group sync.WaitGroup
	for index := 0; index < 1024; index++ {
		group.Add(1)
		go func(index int) {
			defer group.Done()
			scope := parameterCorrectionScope{Provider: "p", BaseURL: "https://example.com", Protocol: "chat", Kind: "chat", Model: fmt.Sprint(index)}
			cache.commit(scope, []parameterCorrectionRule{{Param: "n", Action: parameterCorrectionSet, Value: 1}})
			cache.apply(scope, map[string]any{"n": 2})
		}(index)
	}
	group.Wait()
	if cache.size() > parameterCorrectionCacheCapacity {
		t.Fatalf("cache exceeded capacity: %d", cache.size())
	}
}

func TestOpenAIParameterCorrectionNeverChangesCallerExplicitParameterColdOrHot(t *testing.T) {
	t.Run("cold", func(t *testing.T) {
		var requests atomic.Int32
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			requests.Add(1)
			writeParameterError(w, "n", "Parameter n must be set to 1.")
		}))
		defer server.Close()
		body := map[string]any{"messages": []any{}, "n": 2}
		request := correctionTestRequest(server, body)
		request.OriginalBody = body
		cache := NewParameterCorrectionCache()
		_, err := (OpenAIClient{HTTPClient: server.Client(), Corrections: cache}).Run(context.Background(), request)
		if err == nil || requests.Load() != 1 || cache.size() != 0 {
			t.Fatalf("explicit cold parameter was corrected: requests=%d cache=%d err=%v", requests.Load(), cache.size(), err)
		}
	})

	t.Run("hot", func(t *testing.T) {
		var captured float64
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var body map[string]any
			_ = json.NewDecoder(r.Body).Decode(&body)
			captured, _ = body["n"].(float64)
			writeCorrectionSuccess(w, body["model"])
		}))
		defer server.Close()
		body := map[string]any{"messages": []any{}, "n": 2}
		request := correctionTestRequest(server, body)
		request.OriginalBody = body
		cache := NewParameterCorrectionCache()
		cache.commit(newParameterCorrectionScope(request, "chat.completions"), []parameterCorrectionRule{{Param: "n", Action: parameterCorrectionSet, Value: 1}})
		if _, err := (OpenAIClient{HTTPClient: server.Client(), Corrections: cache}).Run(context.Background(), request); err != nil {
			t.Fatal(err)
		}
		if captured != 2 {
			t.Fatalf("cached correction changed explicit n: %v", captured)
		}
	})
}

func TestOpenAIParameterCorrectionMayChangeGatewayInjectedParameter(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["n"] != float64(1) {
			writeParameterError(w, "n", "Parameter n must be set to 1.")
			return
		}
		writeCorrectionSuccess(w, body["model"])
	}))
	defer server.Close()
	request := correctionTestRequest(server, map[string]any{"messages": []any{}, "n": 2})
	request.OriginalBody = map[string]any{"messages": []any{}}
	if _, err := (OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}).Run(context.Background(), request); err != nil {
		t.Fatal(err)
	}
	if requests.Load() != 2 {
		t.Fatalf("Gateway-injected parameter was not corrected: requests=%d", requests.Load())
	}
}

func TestOpenAIProviderAdaptationRejectsExplicitSemanticChange(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		t.Fatal("semantically incompatible explicit parameter must fail before upstream")
	}))
	defer server.Close()
	body := map[string]any{"messages": []any{}, "reasoning_effort": "none", "temperature": 0.2}
	request := correctionTestRequest(server, body)
	request.OriginalBody = body
	_, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), request)
	if ErrorCode(err) != "invalid_parameter" || ErrorParam(err) != "reasoning_effort" {
		t.Fatalf("expected exact reasoning incompatibility, got %v param=%q", err, ErrorParam(err))
	}
}

func TestOpenAIProviderAdaptationRejectsInexactDeepSeekEffort(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		t.Fatal("inexact DeepSeek effort must fail before upstream")
	}))
	defer server.Close()
	body := map[string]any{"messages": []any{}, "reasoning_effort": "low"}
	request := Request{
		Kind: "chat.completions", Model: "deepseek-v4-pro", Body: body, OriginalBody: body,
		Candidate: store.RuntimeModelCandidate{
			Provider: "deepseek-openai", BaseURL: server.URL, ProviderModelName: "deepseek-v4-pro", Credentials: map[string]any{"apiKey": "test-key"},
		},
	}
	_, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), request)
	if ErrorCode(err) != "invalid_parameter" || ErrorParam(err) != "reasoning_effort" {
		t.Fatalf("expected exact DeepSeek reasoning incompatibility, got %v param=%q", err, ErrorParam(err))
	}
}

func TestResponsesFallbackProtectsExplicitMappedOutputLimitFromCorrection(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["max_completion_tokens"] != float64(128) || body["max_tokens"] != nil {
			t.Fatalf("Responses output limit mapped incorrectly: %+v", body)
		}
		writeParameterError(w, "max_completion_tokens", "Unsupported parameter: max_completion_tokens")
	}))
	defer server.Close()
	body := map[string]any{"input": "hello", "max_output_tokens": 128}
	request := Request{
		Kind: "responses", Model: "demo", Body: body, OriginalBody: body, UpstreamProtocol: ProtocolOpenAIChatCompletions,
		Candidate: store.RuntimeModelCandidate{
			Provider: "openai", BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "test-key"},
		},
	}
	_, err := (OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}).Run(context.Background(), request)
	if err == nil || requests.Load() != 1 {
		t.Fatalf("explicit mapped output limit was silently corrected: requests=%d err=%v", requests.Load(), err)
	}
}

func TestResponsesFallbackProtectsIncludeMappedLogprobsFromCorrection(t *testing.T) {
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		var body map[string]any
		_ = json.NewDecoder(r.Body).Decode(&body)
		if body["logprobs"] != true {
			t.Fatalf("Responses include did not enable Chat logprobs: %+v", body)
		}
		writeParameterError(w, "logprobs", "Unsupported parameter: logprobs")
	}))
	defer server.Close()
	body := map[string]any{"input": "hello", "include": []any{"message.output_text.logprobs"}}
	request := Request{
		Kind: "responses", Model: "demo", Body: body, OriginalBody: body, UpstreamProtocol: ProtocolOpenAIChatCompletions,
		Candidate: store.RuntimeModelCandidate{
			Provider: "openai", BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "test-key"},
		},
	}
	_, err := (OpenAIClient{HTTPClient: server.Client(), Corrections: NewParameterCorrectionCache()}).Run(context.Background(), request)
	if err == nil || requests.Load() != 1 {
		t.Fatalf("explicit include logprobs was silently corrected: requests=%d err=%v", requests.Load(), err)
	}
}
