package openai

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

	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}

	srv := NewServer(config.EdgeOpenAIConf{
		Adapter:    "ollama",
		Target:     "llama-fixed",
		SessionID:  "configured-session",
		TimeoutSec: 42,
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello",
		"instructions":"Use short answers.",
		"max_output_tokens":16,
		"temperature":0.2,
		"top_p":0.9,
		"background":false
	}`))
	w := httptest.NewRecorder()

	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "llama-fixed" {
		t.Fatalf("target: got %q, want llama-fixed", fake.req.Target)
	}
	if fake.req.Prompt != "Use short answers.\n\nsay hello" {
		t.Fatalf("prompt: got %q", fake.req.Prompt)
	}
	options, ok := fake.req.Input["options"].(map[string]any)
	if !ok {
		t.Fatalf("options not passed: %+v", fake.req.Input)
	}
	if options["max_tokens"].(int) != 16 || options["temperature"].(float64) != 0.2 || options["top_p"].(float64) != 0.9 {
		t.Fatalf("unexpected response options: %+v", options)
	}
	if fake.req.TimeoutSec != 42 {
		t.Fatalf("timeout: got %d, want 42", fake.req.TimeoutSec)
	}
	if fake.req.SessionID != "configured-session" {
		t.Fatalf("session_id: got %q, want configured-session", fake.req.SessionID)
	}
	if fake.req.Background {
		t.Fatal("background should remain false for responses")
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.OutputText != "hello world" {
		t.Fatalf("output_text: got %q", resp.OutputText)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "hello world" {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
}

func TestResponsesRejectsUnsupportedRequests(t *testing.T) {
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)

	cases := []struct {
		name   string
		method string
		body   string
		want   int
	}{
		{"wrong method", http.MethodGet, "", http.StatusMethodNotAllowed},
		{"stream=true", http.MethodPost, `{"model":"m","input":"hi","stream":true}`, http.StatusBadRequest},
		{"empty input", http.MethodPost, `{"model":"m","input":""}`, http.StatusBadRequest},
		{"non-string input", http.MethodPost, `{"model":"m","input":["a","b"]}`, http.StatusBadRequest},
		{"options wrapper unsupported", http.MethodPost, `{"model":"m","input":"hi","options":{"max_output_tokens":16}}`, http.StatusBadRequest},
		{"background unsupported", http.MethodPost, `{"model":"m","input":"hi","background":true}`, http.StatusBadRequest},
		{"bad max_output_tokens", http.MethodPost, `{"model":"m","input":"hi","max_output_tokens":0}`, http.StatusBadRequest},
		{"bad top_p", http.MethodPost, `{"model":"m","input":"hi","top_p":1.5}`, http.StatusBadRequest},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/v1/responses", strings.NewReader(tc.body))
			w := httptest.NewRecorder()
			srv.routes().ServeHTTP(w, req)
			if w.Code != tc.want {
				t.Fatalf("got %d want %d body=%s", w.Code, tc.want, w.Body.String())
			}
		})
	}
}

func TestResponsesConfiguredTargetWinsOverRequestModel(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "config-target"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Target != "config-target" {
		t.Fatalf("target: got %q, want config-target", fake.req.Target)
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if resp.Model != "client-model" {
		t.Fatalf("model: got %q, want client-model", resp.Model)
	}
}

func TestResponsesReturnsOpenAICompatibleShape(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !strings.HasPrefix(resp.ID, "resp-") {
		t.Fatalf("id: got %q, want resp- prefix", resp.ID)
	}
	if resp.Model != "client-model" {
		t.Fatalf("model: got %q", resp.Model)
	}
	if resp.OutputText != "answer" {
		t.Fatalf("output_text: got %q", resp.OutputText)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "answer" {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
	if resp.Usage.TotalTokens != 2 {
		t.Fatalf("usage.total_tokens: got %d", resp.Usage.TotalTokens)
	}
}

func TestResponsesSanitizesKnownSentinelToken(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden <|mask_end|>"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer <|mask_end|>"}
	fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello"
	}`))
	w := httptest.NewRecorder()

	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if strings.Contains(resp.OutputText, "<|mask_end|>") || strings.Contains(w.Body.String(), "<|mask_end|>") {
		t.Fatalf("sentinel leaked in responses output: %s", w.Body.String())
	}
	if resp.OutputText != "answer" {
		t.Fatalf("output_text: got %q, want answer", resp.OutputText)
	}
}

func TestResponsesUsageDefaultsToZeroObject(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"m",
		"input":"hi"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	var body map[string]json.RawMessage
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if _, ok := body["usage"]; !ok {
		t.Fatal("usage field missing from response")
	}
	var usage openAIUsage
	if err := json.Unmarshal(body["usage"], &usage); err != nil {
		t.Fatalf("usage is not an object: %s", body["usage"])
	}
}

func TestResponsesStrictOutputNormalizesAgentResponse(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama", StrictOutput: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>\n\nfinish"
	}`))
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if fake.req.Metadata["strict_output"] != "true" {
		t.Fatalf("strict_output metadata: got %q", fake.req.Metadata["strict_output"])
	}
	if fake.req.Input["think"] != false {
		t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
	}
	if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") {
		t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
	}

	var resp responsesResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode: %v", err)
	}
	want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
	if resp.OutputText != want {
		t.Fatalf("output_text:\ngot  %q\nwant %q", resp.OutputText, want)
	}
	if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != want {
		t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
	}
}

func TestResponsesStreamGateNormalizedHandlerUsesSingleTerminalBarrier(t *testing.T) {
	fake := &fakeRunService{events: bufferedRunEvents(
		&iop.RunEvent{Type: "reasoning_delta", Delta: "private reasoning"},
		&iop.RunEvent{Type: "delta", Delta: "hello"},
		&iop.RunEvent{Type: "complete", Metadata: map[string]string{
			runtimeMetadataOpenAIToolCalls: `[{"id":"call-1","type":"function","function":{"name":"lookup","arguments":"{}"}}]`,
		}},
	)}
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "ollama", Target: "served-model",
		StreamEvidenceGate: config.StreamEvidenceGateConf{
			Enabled: true,
			Filters: []config.StreamGateFilterPolicyConf{
				{Filter: config.StreamGateFilterRepeatGuard},
				{Filter: config.StreamGateFilterSchemaGate},
				{Filter: config.StreamGateFilterProviderError},
			},
		},
	}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"say hello",
		"metadata":{"scheme":"object"}
	}`))
	w := newRecordingResponseWriter()
	srv.routes().ServeHTTP(w, req)
	if w.code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.code, w.body.String())
	}
	if w.headerCallCount() != 1 {
		t.Fatalf("WriteHeader calls=%d, want one terminal commit", w.headerCallCount())
	}
	var response responsesResponse
	if err := json.Unmarshal([]byte(w.body.String()), &response); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if response.OutputText != "hello" {
		t.Fatalf("output_text=%q", response.OutputText)
	}
	if len(response.Output) != 2 || response.Output[1].Type != "function_call" || response.Output[1].Name != "lookup" {
		t.Fatalf("responses output shape=%+v", response.Output)
	}
	if strings.Count(w.body.String(), `"object":"response"`) != 1 {
		t.Fatalf("response opening count body=%s", w.body.String())
	}
}
