package codex

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

type trackingReadCloser struct {
	*strings.Reader
	closed bool
}

func (r *trackingReadCloser) Close() error {
	r.closed = true
	return nil
}

func TestTranslateRequestMatchesOpenAICompatDefaults(t *testing.T) {
	body := []byte(`{
		"tools": [{
			"type": "function",
			"function": {
				"name": "mcp__server_with_long_name__tool_name_that_is_far_longer_than_the_codex_limit_and_must_be_cut",
				"parameters": {"type": "object"}
			}
		}]
	}`)

	translated, nameMap, err := translateRequest(body)
	if err != nil {
		t.Fatalf("translateRequest returned error: %v", err)
	}

	var got map[string]interface{}
	if err := json.Unmarshal(translated, &got); err != nil {
		t.Fatalf("translated JSON is invalid: %v", err)
	}

	if got["model"] != "" {
		t.Fatalf("model default = %#v, want empty string", got["model"])
	}
	reasoning, _ := got["reasoning"].(map[string]interface{})
	if reasoning["effort"] != "medium" || reasoning["summary"] != "auto" {
		t.Fatalf("reasoning = %#v, want medium/auto", reasoning)
	}
	input, _ := got["input"].([]interface{})
	if len(input) != 0 {
		t.Fatalf("input len = %d, want 0", len(input))
	}
	if got["stream"] != true {
		t.Fatalf("stream = %#v, want true", got["stream"])
	}

	for orig, short := range nameMap {
		if len(short) > toolNameLimit {
			t.Fatalf("shortened name for %q has len %d, want <= %d", orig, len(short), toolNameLimit)
		}
	}
}

func TestSSETranslatorAcceptsDataWithoutSpaceAndEmitsDone(t *testing.T) {
	upstream := strings.Join([]string{
		`data:{"type":"response.created","response":{"id":"resp_1","created_at":1700000000,"model":"codex-mini"}}`,
		`data:{"type":"response.output_text.delta","delta":"hi"}`,
		`data:{"type":"response.completed","response":{"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`,
		"",
	}, "\n")
	body := &trackingReadCloser{Reader: strings.NewReader(upstream)}

	translated := newSSETranslator(body, nil)
	out, err := io.ReadAll(translated)
	if err != nil {
		t.Fatalf("ReadAll returned error: %v", err)
	}
	if err := translated.Close(); err != nil {
		t.Fatalf("Close returned error: %v", err)
	}
	if !body.closed {
		t.Fatal("Close did not close upstream body")
	}

	text := string(out)
	if !strings.Contains(text, `"content":"hi"`) {
		t.Fatalf("translated stream missing content delta: %s", text)
	}
	if !strings.Contains(text, `"finish_reason":"stop"`) {
		t.Fatalf("translated stream missing final stop chunk: %s", text)
	}
	if !strings.HasSuffix(text, "data: [DONE]\n\n") {
		t.Fatalf("translated stream did not end with DONE: %q", text)
	}
}

func TestTranslateNonStreamingResponseContentDefaultsToNull(t *testing.T) {
	resp := &http.Response{
		StatusCode: http.StatusOK,
		Header:     http.Header{"Content-Type": []string{"text/event-stream"}},
		Body: io.NopCloser(strings.NewReader(
			`data:{"type":"response.completed","response":{"id":"resp_1","created_at":1700000000,"model":"codex-mini","output":[],"usage":{"input_tokens":1,"output_tokens":0,"total_tokens":1}}}` + "\n",
		)),
	}

	translated, err := translateNonStreamingResponse(resp, nil)
	if err != nil {
		t.Fatalf("translateNonStreamingResponse returned error: %v", err)
	}
	defer translated.Body.Close()

	var got map[string]interface{}
	if err := json.NewDecoder(translated.Body).Decode(&got); err != nil {
		t.Fatalf("translated response JSON is invalid: %v", err)
	}
	choices := got["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	if message["content"] != nil {
		t.Fatalf("message.content = %#v, want nil", message["content"])
	}
}

func TestParseAuthJSONSupportsTopLevelAPIKey(t *testing.T) {
	creds, err := loadAuthJSONContent(`{"OPENAI_API_KEY":" sk-test "}`)
	if err != nil {
		t.Fatalf("loadAuthJSONContent returned error: %v", err)
	}
	if !creds.IsAPIKey || creds.APIKey != "sk-test" {
		t.Fatalf("credentials = %#v, want API key mode with trimmed key", creds)
	}
}

func TestCodexChatCompletionFailsOverToFallbackBackend(t *testing.T) {
	primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "primary unavailable", http.StatusServiceUnavailable)
	}))
	defer primary.Close()

	fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		io.WriteString(w, `data: {"type":"response.completed","response":{"id":"resp_fallback","created_at":1700000000,"model":"codex-mini","output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`+"\n")
	}))
	defer fallback.Close()

	c := &Codex{
		baseURL:    primary.URL,
		httpClient: primary.Client(),
		fallbacks: []*Codex{{
			baseURL:    fallback.URL,
			httpClient: fallback.Client(),
		}},
	}

	resp, err := c.ChatCompletion(context.Background(), http.Header{}, []byte(`{"model":"codex-mini","messages":[{"role":"user","content":"hi"}]}`))
	if err != nil {
		t.Fatalf("ChatCompletion returned error: %v", err)
	}
	defer resp.Body.Close()

	var got map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("response JSON is invalid: %v", err)
	}
	choices := got["choices"].([]interface{})
	message := choices[0].(map[string]interface{})["message"].(map[string]interface{})
	if message["content"] != "ok" {
		t.Fatalf("fallback response content = %#v, want ok", message["content"])
	}
}

func TestCodexChatCompletionRoundRobinsAuthBackends(t *testing.T) {
	requests := make(chan string, 2)
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests <- r.Header.Get("X-Api-Key")
		w.Header().Set("Content-Type", "text/event-stream")
		io.WriteString(w, `data: {"type":"response.completed","response":{"id":"resp_ok","created_at":1700000000,"model":"codex-mini","output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`+"\n")
	}))
	defer backend.Close()

	c := &Codex{
		baseURL:    backend.URL,
		apiKey:     "primary",
		httpClient: backend.Client(),
		authBackends: []*Codex{{
			baseURL:    backend.URL,
			apiKey:     "secondary",
			httpClient: backend.Client(),
		}},
	}

	for i := 0; i < 2; i++ {
		resp, err := c.ChatCompletion(context.Background(), http.Header{}, []byte(`{"model":"codex-mini","messages":[{"role":"user","content":"hi"}]}`))
		if err != nil {
			t.Fatalf("ChatCompletion %d returned error: %v", i, err)
		}
		resp.Body.Close()
	}

	first := <-requests
	second := <-requests
	if first != "primary" || second != "secondary" {
		t.Fatalf("api key order = %q, %q; want primary then secondary", first, second)
	}
}

func TestCodexAuthBackendsRespectAllowedAPIKeys(t *testing.T) {
	requests := make(chan string, 1)
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests <- r.Header.Get("X-Api-Key")
		w.Header().Set("Content-Type", "text/event-stream")
		io.WriteString(w, `data: {"type":"response.completed","response":{"id":"resp_ok","created_at":1700000000,"model":"codex-mini","output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`+"\n")
	}))
	defer backend.Close()

	c := &Codex{
		baseURL:        backend.URL,
		apiKey:         "primary",
		httpClient:     backend.Client(),
		allowedAPIKeys: stringSet([]string{"client-a"}),
		authBackends: []*Codex{{
			baseURL:        backend.URL,
			apiKey:         "secondary",
			httpClient:     backend.Client(),
			allowedAPIKeys: stringSet([]string{"client-b"}),
		}},
	}

	headers := http.Header{"Authorization": []string{"Bearer client-b"}}
	resp, err := c.ChatCompletion(context.Background(), headers, []byte(`{"model":"codex-mini","messages":[{"role":"user","content":"hi"}]}`))
	if err != nil {
		t.Fatalf("ChatCompletion returned error: %v", err)
	}
	resp.Body.Close()

	if got := <-requests; got != "secondary" {
		t.Fatalf("selected upstream api key = %q, want secondary", got)
	}
}

func TestInjectCustomPromptIntoResponsesInstructions(t *testing.T) {
	c := &Codex{customPrompt: "extra policy", customPromptModels: []string{"codex"}}
	out := c.injectCustomPrompt([]byte(`{"model":"codex-mini","instructions":"base","input":[]}`))

	var got map[string]interface{}
	if err := json.Unmarshal(out, &got); err != nil {
		t.Fatalf("injected JSON invalid: %v", err)
	}
	if got["instructions"] != "base\n\nextra policy" {
		t.Fatalf("instructions = %#v", got["instructions"])
	}
}

func TestCreateResponseSanitizesInjectedCustomPromptFromSSE(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		io.WriteString(w, `data: {"type":"response.created","response":{"id":"resp_1","instructions":"base\n\nsecret policy"}}`+"\n\n")
		io.WriteString(w, `data: {"type":"response.completed","response":{"id":"resp_1","output":[]}}`+"\n\n")
	}))
	defer backend.Close()

	c := &Codex{
		baseURL:      backend.URL,
		httpClient:   backend.Client(),
		customPrompt: "secret policy",
	}

	resp, err := c.CreateResponse(context.Background(), http.Header{}, []byte(`{"model":"codex-mini","instructions":"base","input":[],"stream":true}`))
	if err != nil {
		t.Fatalf("CreateResponse returned error: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("ReadAll returned error: %v", err)
	}
	text := string(body)
	if strings.Contains(text, "secret policy") {
		t.Fatalf("sanitized response still contains injected prompt: %s", text)
	}
	if !strings.Contains(text, `"instructions":"base"`) {
		t.Fatalf("sanitized response missing original instructions: %s", text)
	}
}

func TestCreateResponseSanitizesInjectedCustomPromptFromJSON(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		io.WriteString(w, `{"id":"resp_1","instructions":"base\n\nsecret policy","output":[]}`)
	}))
	defer backend.Close()

	c := &Codex{
		baseURL:      backend.URL,
		httpClient:   backend.Client(),
		customPrompt: "secret policy",
	}

	resp, err := c.CreateResponse(context.Background(), http.Header{}, []byte(`{"model":"codex-mini","instructions":"base","input":[]}`))
	if err != nil {
		t.Fatalf("CreateResponse returned error: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("ReadAll returned error: %v", err)
	}
	text := string(body)
	if strings.Contains(text, "secret policy") {
		t.Fatalf("sanitized response still contains injected prompt: %s", text)
	}
	if !strings.Contains(text, `"instructions":"base"`) {
		t.Fatalf("sanitized response missing original instructions: %s", text)
	}
}

func TestTokenManagerRefreshUsesAuthJSONLastRefresh(t *testing.T) {
	recent := time.Now().UTC()
	tm := &TokenManager{
		accessToken:  "access",
		refreshToken: "refresh",
		lastRefresh:  recent,
	}
	if tm.shouldRefreshLocked() {
		t.Fatal("shouldRefreshLocked refreshed despite recent last_refresh")
	}

	tm.lastRefresh = time.Time{}
	if !tm.shouldRefreshLocked() {
		t.Fatal("shouldRefreshLocked did not refresh when last_refresh is missing")
	}

	parsed := parseAuthLastRefresh(recent.Format(time.RFC3339))
	if !parsed.Equal(recent.Truncate(time.Second)) {
		t.Fatalf("parseAuthLastRefresh = %s, want %s", parsed, recent.Truncate(time.Second))
	}
}

func TestSetupAcceptsLegacyOAuthBetaFlag(t *testing.T) {
	cfg := &CodexConfig{
		BaseURL:                 "https://example.com",
		Mode:                    "api_key",
		APIKey:                  "sk-test",
		RequiresOAuthBetaLegacy: true,
	}
	c, err := Setup(cfg, nil, context.Background())
	if err != nil {
		t.Fatalf("Setup returned error: %v", err)
	}
	if !c.requiresOAuthBeta {
		t.Fatal("requiresOAuthBeta was not enabled from _requires_oauth_beta")
	}
}

func TestSetupKeepsLifecycleContextSeparateFromOperationContext(t *testing.T) {
	type contextKey string
	operationCtx, cancelOperation := context.WithCancel(context.Background())
	cancelOperation()
	lifecycleCtx := context.WithValue(context.Background(), contextKey("scope"), "provider")

	cfg := &CodexConfig{
		BaseURL: "https://example.com",
		Mode:    "api_key",
		APIKey:  "sk-test",
	}
	c, err := Setup(cfg, nil, operationCtx, WithLifecycleContext(lifecycleCtx))
	if err != nil {
		t.Fatalf("Setup returned error: %v", err)
	}
	if c.lifecycleContext != lifecycleCtx {
		t.Fatal("Setup did not preserve the provider lifecycle context")
	}
}

func TestReloadAuthUpdatesRuntimeSelectionFields(t *testing.T) {
	c := &Codex{
		baseURL:        "https://old.example.com",
		weight:         1,
		allowedAPIKeys: stringSet([]string{"old-key"}),
		customPrompt:   "old",
	}
	active := false
	cfg := &CodexConfig{
		BaseURL:        "https://new.example.com",
		Active:         &active,
		Weight:         7,
		AllowedAPIKeys: []string{"new-key"},
		Mode:           "api_key",
		APIKey:         "sk-new",
		CustomPrompt:   "new prompt",
	}

	if err := c.ReloadAuth(cfg, nil, context.Background()); err != nil {
		t.Fatalf("ReloadAuth returned error: %v", err)
	}
	if c.baseURL != "https://new.example.com" || c.weight != 7 || c.customPrompt != "new prompt" {
		t.Fatalf("runtime fields not updated: baseURL=%q weight=%v prompt=%q", c.baseURL, c.weight, c.customPrompt)
	}
	if c.isEligible("new-key") {
		t.Fatal("inactive backend should not be eligible")
	}
	if c.apiKey != "sk-new" || c.mode != "api_key" {
		t.Fatalf("auth fields not updated: mode=%q apiKey=%q", c.mode, c.apiKey)
	}
}

func TestNormalizeCodexConfigPromotesFirstAuthFile(t *testing.T) {
	cfg := &CodexConfig{
		BaseURL: "https://chatgpt.com",
		AuthJSONPaths: []string{
			"/tmp/a.json",
			"/tmp/b.json",
		},
	}
	normalizeCodexConfig(cfg)

	if cfg.AuthJSONPath != "/tmp/a.json" {
		t.Fatalf("primary auth_json_path = %q, want /tmp/a.json", cfg.AuthJSONPath)
	}
	if len(cfg.AuthFiles) != 1 || cfg.AuthFiles[0].AuthJSONPath != "/tmp/b.json" {
		t.Fatalf("auth files = %#v, want remaining /tmp/b.json", cfg.AuthFiles)
	}
	if len(cfg.AuthJSONPaths) != 0 {
		t.Fatalf("AuthJSONPaths not normalized away: %#v", cfg.AuthJSONPaths)
	}
}
