package backend

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

type imageExtractorRoundTripperFunc func(*http.Request) (*http.Response, error)

func (f imageExtractorRoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

func newHTTPTestImageExtractor(server *httptest.Server) *OpenAIImageTextExtractor {
	return &OpenAIImageTextExtractor{
		endpoint:  server.URL,
		apiKey:    "test-key",
		model:     "test-model",
		prompt:    "test prompt",
		maxTokens: 32,
		client:    server.Client(),
	}
}

func TestImageProviderHTTPFailuresAreClassifiedAndSanitized(t *testing.T) {
	tests := []struct {
		status       int
		wantCategory ImageProviderFailureCategory
	}{
		{http.StatusUnauthorized, ImageProviderFailureAuthRejected},
		{http.StatusForbidden, ImageProviderFailureAuthRejected},
		{http.StatusNotFound, ImageProviderFailureNotFound},
		{http.StatusBadRequest, ImageProviderFailureRequestRejected},
		{http.StatusTooManyRequests, ImageProviderFailureRateLimited},
		{http.StatusInternalServerError, ImageProviderFailureProviderError},
	}
	const providerSecret = "provider-secret-detail-must-not-escape"
	for _, tt := range tests {
		t.Run(http.StatusText(tt.status), func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				w.WriteHeader(tt.status)
				_, _ = io.WriteString(w, `{"error":{"message":"`+providerSecret+`"}}`)
			}))
			defer server.Close()

			_, _, err := newHTTPTestImageExtractor(server).ExtractImageText(context.Background(), ImageExtractRequest{
				ContentType: "image/png",
				Data:        []byte("png"),
			})
			category, status, ok := ImageProviderFailure(err)
			if !ok || category != tt.wantCategory || status != tt.status {
				t.Fatalf("failure = (%q, %d, %v), want (%q, %d, true); err=%v", category, status, ok, tt.wantCategory, tt.status, err)
			}
			if strings.Contains(err.Error(), providerSecret) {
				t.Fatalf("safe error leaked provider response: %q", err)
			}
		})
	}
}

func TestImageProviderInvalidSuccessResponseIsClassified(t *testing.T) {
	tests := []struct {
		name string
		body string
	}{
		{"malformed", `not-json-provider-secret`},
		{"no choices", `{"choices":[]}`},
		{"empty text", `{"choices":[{"message":{"content":""}}]}`},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer server.Close()

			_, _, err := newHTTPTestImageExtractor(server).ExtractImageText(context.Background(), ImageExtractRequest{Data: []byte("png")})
			category, status, ok := ImageProviderFailure(err)
			if !ok || category != ImageProviderFailureInvalidResponse || status != http.StatusOK {
				t.Fatalf("failure = (%q, %d, %v), want (%q, 200, true); err=%v", category, status, ok, ImageProviderFailureInvalidResponse, err)
			}
			if strings.Contains(err.Error(), tt.body) {
				t.Fatalf("safe error leaked provider response: %q", err)
			}
		})
	}
}

func TestImageProviderTimeoutIsClassified(t *testing.T) {
	extractor := &OpenAIImageTextExtractor{
		endpoint:  "https://provider.example.com/v1/chat/completions",
		apiKey:    "test-key",
		model:     "test-model",
		prompt:    "test prompt",
		maxTokens: 32,
		client: &http.Client{Transport: imageExtractorRoundTripperFunc(func(*http.Request) (*http.Response, error) {
			return nil, context.DeadlineExceeded
		})},
	}
	_, _, err := extractor.ExtractImageText(context.Background(), ImageExtractRequest{Data: []byte("png")})
	category, status, ok := ImageProviderFailure(err)
	if !ok || category != ImageProviderFailureTimeout || status != 0 {
		t.Fatalf("failure = (%q, %d, %v), want (%q, 0, true); err=%v", category, status, ok, ImageProviderFailureTimeout, err)
	}
}

func TestValidateOpenAIImageProviderClassifiesEndpointPolicyRejection(t *testing.T) {
	stubExtractorLookupIP(t, func(_ context.Context, _, _ string) ([]net.IP, error) {
		return mustParseIPs(t, "93.184.216.34", "10.0.0.1"), nil
	})

	err := ValidateOpenAIImageProvider(context.Background(), OpenAIImageTextExtractorConfig{
		BaseURL: "https://mixed-addresses.example.com",
		APIKey:  "test-key",
		Model:   "vision-model",
		Timeout: time.Second,
	})
	category, status, ok := ImageProviderFailure(err)
	if !ok || category != ImageProviderFailureRequestRejected || status != 0 {
		t.Fatalf("failure = (%q, %d, %v), want (%q, 0, true); err=%v",
			category, status, ok, ImageProviderFailureRequestRejected, err)
	}
}

func TestValidateOpenAIImageProviderSendsVisionRequest(t *testing.T) {
	var payload struct {
		Model     string `json:"model"`
		MaxTokens int    `json:"max_tokens"`
		Messages  []struct {
			Content []struct {
				Type     string `json:"type"`
				ImageURL struct {
					URL string `json:"url"`
				} `json:"image_url"`
			} `json:"content"`
		} `json:"messages"`
	}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Errorf("decode request: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		_, _ = io.WriteString(w, `{"choices":[{"message":{"content":"OK"}}]}`)
	}))
	defer server.Close()
	extractor := newHTTPTestImageExtractor(server)
	extractor.model = "vision-model"
	extractor.maxTokens = imageProviderValidationMaxTokens

	if err := validateOpenAIImageProvider(context.Background(), extractor); err != nil {
		t.Fatalf("validateOpenAIImageProvider: %v", err)
	}
	if payload.Model != "vision-model" || payload.MaxTokens != 16 {
		t.Fatalf("probe model/max_tokens = %q/%d, want vision-model/16", payload.Model, payload.MaxTokens)
	}
	if len(payload.Messages) != 1 || len(payload.Messages[0].Content) != 2 {
		t.Fatalf("probe messages = %+v, want text and image content", payload.Messages)
	}
	image := payload.Messages[0].Content[1]
	if image.Type != "image_url" || !strings.HasPrefix(image.ImageURL.URL, "data:image/png;base64,") {
		t.Fatalf("probe image content = %+v, want inline PNG", image)
	}
}
