package openai

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

	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

func TestResponsesMetadataIncludesTypedEstimateAndClassification(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"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"client-model",
		"input":"test",
		"metadata":{"request_id":"req-001"}
	}`))
	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())
	}
	estStr := fake.req.Metadata["estimated_input_tokens"]
	if estStr == "" {
		t.Fatalf("estimated_input_tokens should be present")
	}
	ctxClass := fake.req.Metadata["context_class"]
	if ctxClass != "normal" {
		t.Fatalf("context_class: got %q, want normal", ctxClass)
	}
	est := fake.req.EstimatedInputTokens
	if est <= 0 {
		t.Fatalf("EstimatedInputTokens: got %d", est)
	}
	if fake.req.ContextClass != "normal" {
		t.Fatalf("ContextClass: got %q, want normal", fake.req.ContextClass)
	}
}

func TestResponsesMetadataIncludesTypedEstimateAndClassification_LargePayload(t *testing.T) {
	largeInput := make([]byte, 400000)
	for i := range largeInput {
		largeInput[i] = 'x'
	}

	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"}, fake, nil)
	body := `{"model":"client-model","input":"` + string(largeInput) + `"}`
	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
	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.ContextClass != "long" {
		t.Fatalf("context_class: got %q, want long", fake.req.ContextClass)
	}
	if fake.req.EstimatedInputTokens < 100000 {
		t.Fatalf("EstimatedInputTokens: got %d, want >= 100000", fake.req.EstimatedInputTokens)
	}
}

func TestChatCompletionsMetadataIncludesTypedEstimateAndClassification(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"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	estStr := fake.req.Metadata["estimated_input_tokens"]
	if estStr == "" {
		t.Fatalf("estimated_input_tokens should be present")
	}
	ctxClass := fake.req.Metadata["context_class"]
	if ctxClass != "normal" {
		t.Fatalf("context_class: got %q, want normal", ctxClass)
	}
	est := fake.req.EstimatedInputTokens
	if est <= 0 {
		t.Fatalf("EstimatedInputTokens: got %d", est)
	}
	if fake.req.ContextClass != "normal" {
		t.Fatalf("ContextClass: got %q, want normal", fake.req.ContextClass)
	}
}

func TestChatCompletionsAssembledLogs(t *testing.T) {
	core, observed := observer.New(zap.DebugLevel)
	logger := zap.New(core)

	fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "reasoning..."}
	fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{
		"openai_tool_calls": `[{"id":"call_001","type":"function","function":{"name":"get_weather","arguments":"{}"}}]`,
	}}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, logger)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}],
		"tools":[{"type":"function","function":{"name":"get_weather"}}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	found := false
	for _, entry := range observed.All() {
		if entry.Message == "openai chat completion output" {
			found = true
			m := entry.ContextMap()
			if m["response_mode"] != "normalized" {
				t.Errorf("expected response_mode=normalized, got %v", m["response_mode"])
			}
			if m["assembled_content"] != "hello" {
				t.Errorf("expected assembled_content=hello, got %v", m["assembled_content"])
			}
			if m["assembled_reasoning"] != "reasoning..." {
				t.Errorf("expected assembled_reasoning=reasoning..., got %v", m["assembled_reasoning"])
			}
			tc, ok := m["assembled_tool_calls"].([]interface{})
			if !ok || len(tc) != 1 || tc[0] != "get_weather" {
				t.Errorf("expected assembled_tool_calls=[get_weather], got %v", m["assembled_tool_calls"])
			}
			if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
				t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
			}
		}
	}
	if !found {
		t.Error("expected log message 'openai chat completion output' not found")
	}
}

// TestChatCompletionsAssembledLogsNormalized verifies the normalized-path
// completion log reports the internal response_mode execution label
// (normalized) and the assembled observation fields.
func TestChatCompletionsAssembledLogsNormalized(t *testing.T) {
	core, observed := observer.New(zap.DebugLevel)
	logger := zap.New(core)

	fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello normalized"}
	fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "reasoning normalized..."}
	fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{
		"openai_tool_calls": `[{"id":"call_002","type":"function","function":{"name":"run_code","arguments":"{}"}}]`,
	}}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, logger)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"client-model",
		"messages":[{"role":"user","content":"hi"}],
		"tools":[{"type":"function","function":{"name":"run_code"}}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	found := false
	for _, entry := range observed.All() {
		if entry.Message == "openai chat completion output" {
			found = true
			m := entry.ContextMap()
			if m["response_mode"] != "normalized" {
				t.Errorf("expected response_mode=normalized, got %v", m["response_mode"])
			}
			if m["assembled_content"] != "hello normalized" {
				t.Errorf("expected assembled_content=hello normalized, got %v", m["assembled_content"])
			}
			if m["assembled_reasoning"] != "reasoning normalized..." {
				t.Errorf("expected assembled_reasoning=reasoning normalized..., got %v", m["assembled_reasoning"])
			}
			tc, ok := m["assembled_tool_calls"].([]interface{})
			if !ok || len(tc) != 1 || tc[0] != "run_code" {
				t.Errorf("expected assembled_tool_calls=[run_code], got %v", m["assembled_tool_calls"])
			}
			if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
				t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
			}
		}
	}
	if !found {
		t.Error("expected log message 'openai chat completion output' not found")
	}
}

func TestChatCompletionsAssembledLogsPassthrough(t *testing.T) {
	core, observed := observer.New(zap.DebugLevel)
	logger := zap.New(core)

	providerBody := `{"choices":[{"message":{"content":"passthrough content","reasoning_content":"passthrough reasoning","tool_calls":[{"function":{"name":"call_passthrough"}}]}}]}`
	frames := make(chan *iop.ProviderTunnelFrame, 3)
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerBody)}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(frames)

	fake := &providerFakeRunService{
		tunnelFrames: frames,
	}
	catalog := []config.ModelCatalogEntry{
		{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, logger)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-model",
		"messages":[{"role":"user","content":"hello"}]
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	found := false
	for _, entry := range observed.All() {
		if entry.Message == "openai chat completion passthrough closed" {
			found = true
			m := entry.ContextMap()
			if m["assembled_content"] != "passthrough content" {
				t.Errorf("expected assembled_content='passthrough content', got %v", m["assembled_content"])
			}
			if m["assembled_reasoning"] != "passthrough reasoning" {
				t.Errorf("expected assembled_reasoning='passthrough reasoning', got %v", m["assembled_reasoning"])
			}
			tc, ok := m["assembled_tool_calls"].([]interface{})
			if !ok || len(tc) != 1 || tc[0] != "call_passthrough" {
				t.Errorf("expected assembled_tool_calls=[call_passthrough], got %v", m["assembled_tool_calls"])
			}
			if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
				t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
			}
		}
	}
	if !found {
		t.Error("expected log message 'openai chat completion passthrough closed' not found")
	}
}

// TestProviderPoolDispatchLogObservationFields verifies that provider-pool
// dispatch logs carry provider_id, provider_type, execution_path, and
// queue_reason fields populated from the selected candidate (SURFACE_OBS-2).
func TestProviderPoolDispatchLogObservationFields(t *testing.T) {
	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)
	const rawToken = "sk-pool-obs-token"
	const edgeID = "edge-pool-obs-logs"
	const model = "pool-obs-model"

	// Build tunnel frames that complete successfully.
	frames := staticProviderTunnelFrames(`{"choices":[{"message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":4,"completion_tokens":3}}`)

	fake := &providerFakeRunService{
		poolDispatchPath: "provider_tunnel",
		tunnelFrames:     frames,
	}
	srv := NewServer(config.EdgeOpenAIConf{
		PrincipalTokens: []config.OpenAIPrincipalTokenConf{
			{TokenRef: "iop-tok-alice", TokenHashSHA256: sha256Hex(rawToken), PrincipalRef: "user:alice", PrincipalAlias: "alice"},
		},
	}, fake, logger)
	srv.SetEdgeID(edgeID)
	srv.SetModelCatalog([]config.ModelCatalogEntry{
		{ID: model, Providers: map[string]string{"prov-obs": "served-obs"}},
	})

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-obs-model",
		"messages":[{"role":"user","content":"hello"}]
	}`))
	req.Header.Set("Authorization", "Bearer "+rawToken)
	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())
	}

	// Verify the observer logger captured the dispatch log line with the
	// expected provider observation fields. The structured log line from
	// handleChatCompletionsProviderPool must carry provider_id, provider_type,
	// execution_path, adapter, target, and queue_reason.
	var found bool
	for _, entry := range observed.All() {
		if entry.Message != "openai chat completion provider-pool dispatch" {
			continue
		}
		found = true
		// Verify provider observation fields exist on the structured log.
		if _, ok := entry.ContextMap()["provider_id"]; !ok {
			t.Error("dispatch log missing provider_id field")
		}
		if _, ok := entry.ContextMap()["provider_type"]; !ok {
			t.Error("dispatch log missing provider_type field")
		}
		if _, ok := entry.ContextMap()["execution_path"]; !ok {
			t.Error("dispatch log missing execution_path field")
		}
		if _, ok := entry.ContextMap()["queue_reason"]; !ok {
			t.Error("dispatch log missing queue_reason field")
		}
		if _, ok := entry.ContextMap()["adapter"]; !ok {
			t.Error("dispatch log missing adapter field")
		}
		if _, ok := entry.ContextMap()["target"]; !ok {
			t.Error("dispatch log missing target field")
		}
	}
	if !found {
		t.Fatal("expected provider-pool dispatch log line in observer")
	}
}

// TestResponsesProviderPoolTunnelMetadataAndContextPropagation verifies that the
// provider-pool tunnel path preserves metadata, estimated_input_tokens, and
// context_class from the base request into the tunnel dispatch (SDD S02/S03).
func TestResponsesProviderPoolTunnelMetadataAndContextPropagation(t *testing.T) {
	frames := make(chan *iop.ProviderTunnelFrame, 3)
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(`{"ok":true}`)}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(frames)

	fake := &providerFakeRunService{
		poolDispatchPath:   string(edgeservice.ProviderPoolPathTunnel),
		tunnelFrames:       frames,
		tunnelServedTarget: "served-model",
	}

	catalog := []config.ModelCatalogEntry{
		{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"pool-model",
		"input":"hello",
		"metadata":{"experiment":"pool-test"}
	}`))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

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

	tunnelReqs := fake.tunnelReqsSnapshot()
	if len(tunnelReqs) != 1 {
		t.Fatalf("expected one tunnel req, got %d", len(tunnelReqs))
	}
	tunnelReq := tunnelReqs[0]

	// Metadata must contain the principal and pool-echoed fields.
	if tunnelReq.Metadata == nil {
		t.Fatalf("tunnel metadata must not be nil")
	}
	if tunnelReq.Metadata["openai_model"] == "" {
		t.Error("openai_model missing from tunnel metadata")
	}
	if tunnelReq.Metadata["openai_stream"] == "" {
		t.Error("openai_stream missing from tunnel metadata")
	}
	if tunnelReq.Metadata["estimated_input_tokens"] == "" {
		t.Error("estimated_input_tokens missing from tunnel metadata")
	}
	if tunnelReq.Metadata["context_class"] == "" {
		t.Error("context_class missing from tunnel metadata")
	}
	if tunnelReq.Metadata["experiment"] != "pool-test" {
		t.Errorf("caller metadata lost: got %v", tunnelReq.Metadata)
	}

	// EstimatedInputTokens and ContextClass must be non-zero/non-empty on the
	// tunnel request itself, so Node observability preserves the same values
	// as the direct tunnel path.
	if tunnelReq.EstimatedInputTokens <= 0 {
		t.Errorf("EstimatedInputTokens must be > 0: got %d", tunnelReq.EstimatedInputTokens)
	}
	if tunnelReq.ContextClass == "" {
		t.Error("ContextClass must be non-empty on tunnel request")
	}
}
