package openai

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

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

// TestResponsesProtocolProfileOperationPassthrough verifies that an explicit
// openai_responses profile admits the responses operation through the
// provider-pool tunnel and that the tunnel request carries the canonical
// operation identifier.
func TestResponsesProtocolProfileOperationPassthrough(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile: %v", err)
	}

	fake := &providerFakeRunService{
		tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-1","object":"response","output":[]}`),
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-openai",
			ProfileID:           profile.ID,
			ProfileDriver:       string(profile.Driver),
			ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile:     &profile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-model",
		Providers: map[string]string{"prov-openai": "gpt-4-responses"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

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

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if len(fake.reqsSnapshot()) != 0 {
		t.Fatalf("profile-backed responses must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
	}
	reqs := fake.tunnelReqsSnapshot()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
	}
	got := reqs[0]
	if got.Operation != string(config.OperationResponses) {
		t.Fatalf("tunnel operation: got %q want %q", got.Operation, config.OperationResponses)
	}
	if got.Path != "/v1/responses" {
		t.Fatalf("tunnel path: got %q want /v1/responses", got.Path)
	}
	if got.Method != http.MethodPost {
		t.Fatalf("tunnel method: got %q want POST", got.Method)
	}
	if got.ProviderID != "prov-openai" {
		t.Fatalf("selected provider: got %q want prov-openai", got.ProviderID)
	}
}

// TestResponsesProtocolProfileOperationPassthroughNonStream verifies that the
// responses profile operation admits non-streaming passthrough and that the
// provider body preserves the caller's Responses-shaped fields verbatim.
func TestResponsesProtocolProfileOperationPassthroughNonStream(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile: %v", err)
	}

	fake := &providerFakeRunService{
		tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-ns","object":"response","output":[]}`),
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-openai-ns",
			ProfileID:           profile.ID,
			ProfileDriver:       string(profile.Driver),
			ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile:     &profile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-ns",
		Providers: map[string]string{"prov-openai-ns": "gpt-4-ns"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"responses-ns",
		"input":"non-stream input",
		"max_output_tokens":128
	}`))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	bodies := fake.tunnelBodiesSnapshot()
	if len(bodies) != 1 {
		t.Fatalf("expected 1 tunnel body, got %d", len(bodies))
	}
	var providerReq map[string]any
	if err := json.Unmarshal(bodies[0], &providerReq); err != nil {
		t.Fatalf("provider body JSON: %v", err)
	}
	if providerReq["max_output_tokens"].(float64) != 128 {
		t.Fatalf("max_output_tokens must be preserved: %+v", providerReq)
	}
	if _, ok := providerReq["stream"]; ok {
		t.Fatalf("stream must not be injected on passthrough: %+v", providerReq)
	}
}

// TestResponsesProtocolProfileOperationPassthroughStream verifies that the
// responses profile operation admits stream=true passthrough and that the
// provider tunnel carries the stream flag to the upstream provider.
func TestResponsesProtocolProfileOperationPassthroughStream(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile: %v", err)
	}

	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(`{"id":"resp-stream","object":"response","output":[],"delta":"chunk-1"}`)}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(frames)

	fake := &providerFakeRunService{
		tunnelFrames: frames,
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-openai-stream",
			ProfileID:           profile.ID,
			ProfileDriver:       string(profile.Driver),
			ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile:     &profile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-stream",
		Providers: map[string]string{"prov-openai-stream": "gpt-4-stream"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"responses-stream",
		"input":"stream this",
		"stream":true
	}`))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	reqs := fake.tunnelReqsSnapshot()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
	}
	if !reqs[0].Stream {
		t.Fatal("stream tunnel request must carry stream=true")
	}
}

// TestResponsesProtocolProfileUnsupportedRejectsBeforeDispatch verifies that an
// explicit profile which does not declare the responses operation is rejected
// before any candidate selection or dispatch occurs. The handler maps the typed
// operation admission failure to HTTP 400 with invalid_request_error and the
// sanitized message "no provider profile supports the requested Responses operation".
func TestResponsesProtocolProfileUnsupportedRejectsBeforeDispatch(t *testing.T) {
	// Anthropic profile does not declare the responses operation.
	anthropicProfile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile anthropic: %v", err)
	}

	fake := &providerFakeRunService{
		poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-anthropic",
			ProfileID:           anthropicProfile.ID,
			ProfileDriver:       string(anthropicProfile.Driver),
			ProfileCapabilities: append([]string(nil), anthropicProfile.Capabilities...),
			ProtocolProfile:     &anthropicProfile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-model",
		Providers: map[string]string{"prov-anthropic": "claude-3"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"responses-model",
		"input":"should be rejected"
	}`))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected HTTP 400 for unsupported operation, got %d body=%s", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("unmarshal error response: %v", err)
	}
	errObj, ok := resp["error"].(map[string]any)
	if !ok {
		t.Fatalf("error object missing: %v", resp)
	}
	if errObj["type"] != "invalid_request_error" {
		t.Fatalf("error.type = %v, want invalid_request_error", errObj["type"])
	}
	if errObj["message"] != "no provider profile supports the requested Responses operation" {
		t.Fatalf("error.message = %v, want 'no provider profile supports the requested Responses operation'", errObj["message"])
	}
	if len(fake.tunnelReqsSnapshot()) != 0 {
		t.Fatalf("unsupported profile must not dispatch any tunnel request, got %d", len(fake.tunnelReqsSnapshot()))
	}
	if len(fake.reqsSnapshot()) != 0 {
		t.Fatalf("unsupported profile must not dispatch any normalized run, got %d", len(fake.reqsSnapshot()))
	}
}

// TestResponsesLegacyProfileFallback verifies that a legacy candidate without a
// profile still falls back to the path-based dispatch and carries the
// /v1/responses path for tunnel passthrough.
func TestResponsesLegacyProfileFallback(t *testing.T) {
	fake := &providerFakeRunService{
		tunnelFrames:       staticProviderTunnelFrames(`{"id":"resp-legacy","object":"response","output":[]}`),
		tunnelServedTarget: "legacy-served",
	}

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

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

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if len(fake.reqsSnapshot()) != 0 {
		t.Fatalf("legacy fallback must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
	}
	reqs := fake.tunnelReqsSnapshot()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
	}
	got := reqs[0]
	if got.Path != "/v1/responses" {
		t.Fatalf("legacy tunnel path: got %q want /v1/responses", got.Path)
	}
	if got.Operation != string(config.OperationResponses) {
		t.Fatalf("legacy tunnel operation: got %q want %q", got.Operation, config.OperationResponses)
	}
}

// TestResponsesProtocolProfileProviderAuthInjection verifies that provider auth
// headers configured via the request header are injected into the tunnel
// request when the selected candidate carries an explicit profile.
func TestResponsesProtocolProfileProviderAuthInjection(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile: %v", err)
	}

	fake := &providerFakeRunService{
		tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-auth","object":"response","output":[]}`),
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-openai-auth",
			ProfileID:           profile.ID,
			ProfileDriver:       string(profile.Driver),
			ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile:     &profile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-auth",
		Providers: map[string]string{"prov-openai-auth": "gpt-4-auth"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"responses-auth",
		"input":"auth input"
	}`))
	req.Header.Set("X-IOP-Provider-Authorization", "request-provider-secret")
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	reqs := fake.tunnelReqsSnapshot()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
	}
	got := reqs[0]
	if got.Headers["Authorization"] != "Bearer request-provider-secret" {
		t.Fatalf("provider auth header: got %q, want %q", got.Headers["Authorization"], "Bearer request-provider-secret")
	}
}

// TestResponsesProtocolProfileProviderAuthRequiredMissing verifies that a
// required provider auth header missing from the request is rejected with a
// 400 before any tunnel dispatch is sent.
func TestResponsesProtocolProfileProviderAuthRequiredMissing(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile: %v", err)
	}

	fake := &providerFakeRunService{
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ProviderID:          "prov-openai-auth-req",
			ProfileID:           profile.ID,
			ProfileDriver:       string(profile.Driver),
			ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile:     &profile,
		},
	}

	catalog := []config.ModelCatalogEntry{{
		ID:        "responses-auth-req",
		Providers: map[string]string{"prov-openai-auth-req": "gpt-4-auth-req"},
	}}
	srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
		"model":"responses-auth-req",
		"input":"missing auth"
	}`))
	w := httptest.NewRecorder()
	srv.handleResponses(w, req)

	if w.Code == http.StatusOK {
		t.Fatalf("expected 400 for missing required provider auth, got %d", w.Code)
	}
	if len(fake.tunnelReqsSnapshot()) != 0 {
		t.Fatalf("missing auth must not dispatch any tunnel request, got %d", len(fake.tunnelReqsSnapshot()))
	}
}

// TestResponsesProtocolProfileOperationAdmissionTable verifies the operation
// admission behavior across supported/unsupported/nil-profile scenarios by
// driving each case through the handler and asserting the dispatch outcome.
func TestResponsesProtocolProfileOperationAdmissionTable(t *testing.T) {
	openaiProfile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile openai: %v", err)
	}
	anthropicProfile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatalf("ResolveProtocolProfile anthropic: %v", err)
	}

	cases := []struct {
		name             string
		profile          *config.ConcreteProtocolProfile
		profileID        string
		expectedStatus   int
		expectedErrType  string
		expectedErrMsg   string
		expectTunnelCall bool
	}{
		{
			name:             "openai profile admits responses operation",
			profile:          &openaiProfile,
			profileID:        "openai",
			expectedStatus:   http.StatusOK,
			expectTunnelCall: true,
		},
		{
			name:             "anthropic profile rejects responses operation",
			profile:          &anthropicProfile,
			profileID:        "anthropic",
			expectedStatus:   http.StatusBadRequest,
			expectedErrType:  "invalid_request_error",
			expectedErrMsg:   "no provider profile supports the requested Responses operation",
			expectTunnelCall: false,
		},
		{
			name:             "nil profile (legacy) admits responses via path fallback",
			profile:          nil,
			profileID:        "",
			expectedStatus:   http.StatusOK,
			expectTunnelCall: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			fake := &providerFakeRunService{
				poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
				tunnelFrames:     staticProviderTunnelFrames(`{"id":"resp-table","object":"response","output":[]}`),
				poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
					ProviderID:      "prov-table",
					ProfileID:       tc.profileID,
					ProfileDriver:   "",
					ProtocolProfile: tc.profile,
				},
			}

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

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

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

			if tc.expectedErrType != "" {
				var resp map[string]any
				if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
					t.Fatalf("unmarshal error response: %v", err)
				}
				errObj, ok := resp["error"].(map[string]any)
				if !ok {
					t.Fatalf("error object missing: %v", resp)
				}
				if errObj["type"] != tc.expectedErrType {
					t.Fatalf("error.type = %v, want %s", errObj["type"], tc.expectedErrType)
				}
				if errObj["message"] != tc.expectedErrMsg {
					t.Fatalf("error.message = %v, want %s", errObj["message"], tc.expectedErrMsg)
				}
			}

			tunnelReqs := fake.tunnelReqsSnapshot()
			if tc.expectTunnelCall && len(tunnelReqs) != 1 {
				t.Fatalf("expected 1 tunnel call, got %d", len(tunnelReqs))
			}
			if !tc.expectTunnelCall && len(tunnelReqs) != 0 {
				t.Fatalf("expected 0 tunnel calls, got %d", len(tunnelReqs))
			}
		})
	}
}
