package openai

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

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

// chatProviderAuthServer builds a provider-pool tunnel server with the given
// provider auth forwarding config, mirroring chatSidebandServer's catalog so
// pool-model routes over the provider tunnel path.
func chatProviderAuthServer(frames chan *iop.ProviderTunnelFrame, auth config.EdgeOpenAIProviderAuthConf) (*Server, *providerFakeRunService) {
	fake := &providerFakeRunService{
		poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
		tunnelFrames:     frames,
	}
	srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: auth}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{
		{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
	})
	return srv, fake
}

func staticOKTunnelFrames(body string) chan *iop.ProviderTunnelFrame {
	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(body)}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(frames)
	return frames
}

// TestChatProviderTunnelForwardsProviderAuthHeader verifies SDD S03: a raw user
// token supplied in the configured provider auth request header is forwarded to
// the provider tunnel request as the configured target header, formatted with
// the configured scheme, while already-prefixed values are preserved.
func TestChatProviderTunnelForwardsProviderAuthHeader(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	for _, tc := range []struct {
		name       string
		callerAuth string
		wantHeader string
	}{
		{name: "raw token gets scheme", callerAuth: "user-token", wantHeader: "Bearer user-token"},
		{name: "already prefixed preserved", callerAuth: "Bearer user-token", wantHeader: "Bearer user-token"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
			req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
				"model":"pool-model",
				"messages":[{"role":"user","content":"hello"}]
			}`))
			req.Header.Set("X-IOP-Provider-Authorization", tc.callerAuth)
			w := httptest.NewRecorder()
			srv.handleChatCompletions(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))
			}
			// The handler sets auth headers on the tunnel request in the
			// tunnel path of handleChatCompletionsProviderPool.
			if got := reqs[0].Headers["Authorization"]; got != tc.wantHeader {
				t.Fatalf("provider Authorization header: got %q, want %q", got, tc.wantHeader)
			}
		})
	}
}

// TestChatProviderTunnelProviderAuthRequired verifies SDD S03: a missing
// required provider auth header is rejected. For provider-pool tunnel path,
// auth is checked after SubmitProviderPool returns. The tunnel handle is closed
// and a 400 is returned (SDD D01).
func TestChatProviderTunnelProviderAuthRequired(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
	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)

	// Auth required check happens in the tunnel path of handleChatCompletionsProviderPool.
	// Since auth is missing, we get 400 and no tunnel request is sent (SDD S03 pre-dispatch).
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if strings.Contains(w.Body.String(), "user-token") {
		t.Fatalf("error body must not echo a raw token: %s", w.Body.String())
	}
	// No tunnel request should be recorded: PrepareTunnel failed before dispatch.
	if got := len(fake.tunnelReqsSnapshot()); got != 0 {
		t.Fatalf("missing required provider auth must not dispatch tunnel: got %d tunnel requests", got)
	}
}

// TestChatCompletionsProviderPoolTunnelProviderAuthForwardedBeforeDispatch verifies
// SDD S03: when provider auth is configured and present, the auth header lands on
// the actual tunnel request that the service sends, not via late handle mutation.
// The auth check runs inside SubmitProviderPool via PrepareTunnel before the
// tunnel request is built and dispatched.
func TestChatCompletionsProviderPoolTunnelProviderAuthForwardedBeforeDispatch(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-model",
		"messages":[{"role":"user","content":"hello"}]
	}`))
	req.Header.Set("X-IOP-Provider-Authorization", "user-token")
	w := httptest.NewRecorder()
	srv.handleChatCompletions(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))
	}
	// Auth header must be on the recorded tunnel request (i.e. pre-dispatch),
	// not on a late handle mutation.
	if got := reqs[0].Headers["Authorization"]; got != "Bearer user-token" {
		t.Fatalf("tunnel request Authorization header: got %q, want %q", got, "Bearer user-token")
	}
}

// TestChatCompletionsProviderPoolOllamaSelectionAllowsMissingProviderAuth verifies
// SDD D01: a missing required provider auth does NOT block the normalized
// (Ollama/CLI) execution path. Only tunnel dispatch requires the auth header.
func TestChatCompletionsProviderPoolOllamaSelectionAllowsMissingProviderAuth(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	// Ollama path: poolDispatchPath=normalized, no tunnel frames needed.
	fake := &providerFakeRunService{
		poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
	}
	catalog := []config.ModelCatalogEntry{
		{ID: "ollama-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
	}
	srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: auth}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"ollama-model",
		"messages":[{"role":"user","content":"hello"}]
	}`))
	// No X-IOP-Provider-Authorization header — should still succeed.
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	// No tunnel dispatch for Ollama-normalized path.
	reqs := fake.tunnelReqsSnapshot()
	if len(reqs) != 0 {
		t.Fatalf("expected 0 tunnel dispatches for Ollama path, got %d", len(reqs))
	}
}

// TestChatProviderTunnelProviderAuthDoesNotReplaceInboundAuth verifies the
// provider token is sourced only from the configured provider auth header, not
// from the inbound IOP Authorization header, keeping IOP inbound auth and
// outbound provider auth separate.
func TestChatProviderTunnelProviderAuthDoesNotReplaceInboundAuth(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-model",
		"messages":[{"role":"user","content":"hello"}]
	}`))
	// Inbound IOP auth and the configured provider auth header carry different
	// tokens; the provider header must come from the provider auth header only.
	req.Header.Set("Authorization", "Bearer iop-inbound-token")
	req.Header.Set("X-IOP-Provider-Authorization", "provider-token")
	w := httptest.NewRecorder()
	srv.handleChatCompletions(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))
	}
	// The tunnel auth headers should come from the provider auth header only,
	// not from the inbound IOP Authorization header.
	if got := reqs[0].Headers["Authorization"]; got != "Bearer provider-token" {
		t.Fatalf("provider Authorization must derive from provider auth header, got %q", got)
	}
}

// TestResponsesProviderTunnelForwardsProviderAuthHeader verifies the provider
// auth helper is applied to the Responses tunnel: a raw request-header token is
// forwarded as the configured target header, and a missing required token is
// rejected before dispatch.
func TestResponsesProviderTunnelForwardsProviderAuthHeader(t *testing.T) {
	auth := config.EdgeOpenAIProviderAuthConf{
		Enabled:      true,
		FromHeader:   "X-IOP-Provider-Authorization",
		TargetHeader: "Authorization",
		Scheme:       "Bearer",
		Required:     true,
	}
	t.Run("forwards raw token with scheme", func(t *testing.T) {
		srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
		req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"pool-model","input":"hi"}`))
		req.Header.Set("X-IOP-Provider-Authorization", "user-token")
		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 got := reqs[0].Headers["Authorization"]; got != "Bearer user-token" {
			t.Fatalf("provider Authorization header: got %q want %q", got, "Bearer user-token")
		}
	})
	t.Run("missing required token rejected", func(t *testing.T) {
		srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
		req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"pool-model","input":"hi"}`))
		w := httptest.NewRecorder()
		srv.handleResponses(w, req)

		if w.Code != http.StatusBadRequest {
			t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
		}
		if got := len(fake.tunnelReqsSnapshot()); got != 0 {
			t.Fatalf("missing required provider auth must not dispatch: got %d tunnel requests", got)
		}
	})
}
