package clients

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

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func TestVectorizerClientMultipartAndPrivateResumeState(t *testing.T) {
	var receivedPath string
	var receivedFields map[string]string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		receivedPath = r.URL.Path
		if username, password, ok := r.BasicAuth(); !ok || username != "id" || password != "secret" {
			t.Fatalf("unexpected vectorizer auth")
		}
		if err := r.ParseMultipartForm(1 << 20); err != nil {
			t.Fatalf("parse multipart: %v", err)
		}
		receivedFields = map[string]string{}
		for key, values := range r.MultipartForm.Value {
			receivedFields[key] = values[0]
		}
		w.Header().Set("Content-Type", "image/svg+xml")
		w.Header().Set("X-Image-Token", "private-image-token")
		w.Header().Set("X-Receipt", "private-receipt")
		_, _ = io.WriteString(w, `<svg xmlns="http://www.w3.org/2000/svg"/>`)
	}))
	defer server.Close()

	privateState := map[string]any{}
	response, err := (VectorizerClient{LookupIP: func(context.Context, string) ([]net.IPAddr, error) {
		return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil
	}}).Run(context.Background(), Request{
		Kind: "images.vectorize", Model: "easy-image-vectorizer-1",
		Body:                  map[string]any{"source": map[string]any{"url": "https://example.com/input.png"}, "format": "svg", "maxColors": 16, "cleanupLevel": "strong"},
		Candidate:             storeCandidate(server.URL, "vectorizer", map[string]any{"accessKey": "id", "secretKey": "secret"}),
		OnRemoteTaskSubmitted: func(_ string, payload map[string]any) error { privateState = payload; return nil },
	})
	if err != nil {
		t.Fatalf("run vectorizer: %v", err)
	}
	if receivedPath != "/vectorize" || receivedFields["image.url"] == "" || receivedFields["output.file_format"] != "svg" || receivedFields["processing.max_colors"] != "16" {
		t.Fatalf("unexpected multipart request: path=%s fields=%+v", receivedPath, receivedFields)
	}
	if privateState["imageToken"] != "private-image-token" || privateState["receipt"] != "private-receipt" {
		t.Fatalf("private resume state missing: %+v", privateState)
	}
	serialized := strings.ToLower(toJSONForTest(response.Result))
	if strings.Contains(serialized, "private-image-token") || strings.Contains(serialized, "private-receipt") {
		t.Fatalf("private vectorizer state leaked into response: %s", serialized)
	}
}

func TestVectorizerClientRejectsPrivateSourceBeforeProviderCall(t *testing.T) {
	providerCalls := 0
	server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { providerCalls++ }))
	defer server.Close()
	_, err := (VectorizerClient{LookupIP: func(context.Context, string) ([]net.IPAddr, error) {
		return []net.IPAddr{{IP: net.ParseIP("127.0.0.1")}}, nil
	}}).Run(context.Background(), Request{
		Body:      map[string]any{"source": map[string]any{"url": "https://assets.example.test/input.png"}},
		Candidate: storeCandidate(server.URL, "vectorizer", map[string]any{"apiKey": "key"}),
	})
	if err == nil || !strings.Contains(err.Error(), "blocked network") {
		t.Fatalf("expected blocked source error, got %v", err)
	}
	if providerCalls != 0 {
		t.Fatalf("provider received %d calls for blocked source", providerCalls)
	}
}

func TestVectorizerClientReusesImageTokenForExtraFormat(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/download" {
			t.Fatalf("unexpected path %s", r.URL.Path)
		}
		if err := r.ParseMultipartForm(1 << 20); err != nil {
			t.Fatal(err)
		}
		if r.FormValue("image.token") != "token" || r.FormValue("receipt") != "receipt" || r.FormValue("output.file_format") != "pdf" {
			t.Fatalf("unexpected reuse form: %+v", r.MultipartForm.Value)
		}
		w.Header().Set("Content-Type", "application/pdf")
		_, _ = w.Write([]byte("%PDF-1.7\n"))
	}))
	defer server.Close()
	response, err := (VectorizerClient{}).Run(context.Background(), Request{
		Body:      map[string]any{"_vectorizer_image_token": "token", "_vectorizer_receipt": "receipt", "format": "pdf"},
		Candidate: storeCandidate(server.URL, "vectorizer", map[string]any{"apiKey": "key"}),
	})
	if err != nil {
		t.Fatal(err)
	}
	item := response.Result["data"].([]any)[0].(map[string]any)
	if item["type"] != "file" || item["mime_type"] != "application/pdf" {
		t.Fatalf("unexpected pdf result: %+v", item)
	}
}

func TestTopazClientUploadsPartsPollsAndValidatesOutput(t *testing.T) {
	var mu sync.Mutex
	called := map[string]int{}
	var server *httptest.Server
	server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		called[r.Method+" "+r.URL.Path]++
		mu.Unlock()
		switch {
		case r.Method == http.MethodGet && r.URL.Path == "/source.mp4":
			_, _ = w.Write([]byte("small-video-source"))
		case r.Method == http.MethodPost && r.URL.Path == "/video/":
			if r.Header.Get("X-API-Key") != "topaz-key" {
				t.Fatalf("missing Topaz API key")
			}
			_, _ = io.WriteString(w, `{"requestId":"job-1"}`)
		case r.Method == http.MethodPatch && r.URL.Path == "/video/job-1/accept":
			_, _ = io.WriteString(w, `{"urls":["`+server.URL+`/upload/1","`+server.URL+`/upload/2"]}`)
		case r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/upload/"):
			w.Header().Set("ETag", `"etag-`+strings.TrimPrefix(r.URL.Path, "/upload/")+`"`)
			w.WriteHeader(http.StatusOK)
		case r.Method == http.MethodPatch && r.URL.Path == "/video/job-1/complete-upload/":
			w.WriteHeader(http.StatusOK)
			_, _ = io.WriteString(w, `{}`)
		case r.Method == http.MethodGet && r.URL.Path == "/video/job-1/status":
			_, _ = io.WriteString(w, `{"status":"completed","download":{"url":"`+server.URL+`/output.mp4"}}`)
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	phases := []string{}
	client := TopazClient{Probe: func(_ context.Context, target string) (TopazVideoMetadata, error) {
		if strings.HasPrefix(target, "http") {
			return TopazVideoMetadata{Width: 640, Height: 360, Duration: 3, FrameRate: 24, HasAudio: true}, nil
		}
		return TopazVideoMetadata{Width: 320, Height: 180, Duration: 3, FrameRate: 24, FrameCount: 72, HasAudio: true}, nil
	}}
	response, err := client.Run(context.Background(), Request{
		Model:     "easy-proteus-standard-4",
		Body:      map[string]any{"video_url": server.URL + "/source.mp4", "output_width": 640, "output_height": 360, "preserve_audio": true},
		Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{"allowPrivateSourceDownloads": true, "pollIntervalMs": 1, "pollTimeoutMs": 100}),
		OnRemoteTaskSubmitted: func(_ string, payload map[string]any) error {
			phases = append(phases, payload["phase"].(string))
			return nil
		},
		OnRemoteTaskPolled: func(_ string, payload map[string]any) error {
			phases = append(phases, payload["phase"].(string))
			return nil
		},
	})
	if err != nil {
		t.Fatalf("run Topaz: %v", err)
	}
	if response.RequestID != "job-1" || !containsStringForTest(phases, "created") || !containsStringForTest(phases, "uploaded") {
		t.Fatalf("unexpected Topaz response/phases: response=%+v phases=%+v", response, phases)
	}
	if called["PUT /upload/1"] != 1 || called["PUT /upload/2"] != 1 || called["PATCH /video/job-1/complete-upload/"] != 1 {
		t.Fatalf("upload lifecycle incomplete: %+v", called)
	}
}

func TestTopazClientResumesUploadedTaskWithoutDownloadingSource(t *testing.T) {
	var sourceCalls int
	var server *httptest.Server
	server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/source.mp4" {
			sourceCalls++
		}
		if r.URL.Path == "/video/job-resume/status" {
			_, _ = io.WriteString(w, `{"status":"completed","download":{"url":"`+server.URL+`/result.mp4"}}`)
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()
	_, err := (TopazClient{Probe: func(context.Context, string) (TopazVideoMetadata, error) {
		return TopazVideoMetadata{Width: 640, Height: 360, Duration: 3, FrameRate: 24}, nil
	}}).Run(context.Background(), Request{
		RemoteTaskID: "job-resume", RemoteTaskPayload: map[string]any{"phase": "uploaded", "targetResolution": map[string]any{"width": 640, "height": 360}},
		Body:      map[string]any{"video_url": server.URL + "/source.mp4"},
		Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{"pollIntervalMs": 1, "pollTimeoutMs": 100}),
	})
	if err != nil {
		t.Fatal(err)
	}
	if sourceCalls != 0 {
		t.Fatalf("resume downloaded source %d times", sourceCalls)
	}
}

func TestTopazClientReportsProviderFailureAndPollingTimeout(t *testing.T) {
	tests := []struct {
		name       string
		statusBody string
		timeoutMS  int
		want       string
	}{
		{name: "provider failure", statusBody: `{"status":"failed","message":"upstream rejected"}`, timeoutMS: 100, want: "upstream rejected"},
		{name: "poll timeout", statusBody: `{"status":"processing"}`, timeoutMS: 3, want: "polling timed out"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.URL.Path != "/video/job/status" {
					http.NotFound(w, r)
					return
				}
				_, _ = io.WriteString(w, test.statusBody)
			}))
			defer server.Close()
			_, err := (TopazClient{}).Run(context.Background(), Request{
				RemoteTaskID: "job", RemoteTaskPayload: map[string]any{"phase": "uploaded", "targetResolution": map[string]any{"width": 640, "height": 360}},
				Body: map[string]any{"video_url": "https://assets.example.test/source.mp4"},
				Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{
					"pollIntervalMs": 1, "pollTimeoutMs": test.timeoutMS,
				}),
			})
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("expected %q error, got %v", test.want, err)
			}
		})
	}
}

func TestTopazClientRejectsOversizedInputBeforeProbe(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/source.mp4" {
			_, _ = io.WriteString(w, "too-large")
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()
	probeCalls := 0
	_, err := (TopazClient{Probe: func(context.Context, string) (TopazVideoMetadata, error) {
		probeCalls++
		return TopazVideoMetadata{}, nil
	}}).Run(context.Background(), Request{
		Body: map[string]any{"video_url": server.URL + "/source.mp4"},
		Candidate: storeCandidateWithConfig(server.URL, "topaz", map[string]any{"apiKey": "topaz-key"}, map[string]any{
			"allowPrivateSourceDownloads": true, "maxInputBytes": 3,
		}),
	})
	if err == nil || !strings.Contains(err.Error(), "size limit") {
		t.Fatalf("expected input size limit error, got %v", err)
	}
	if probeCalls != 0 {
		t.Fatalf("oversized input was probed %d times", probeCalls)
	}
}

func storeCandidate(baseURL, specType string, credentials map[string]any) store.RuntimeModelCandidate {
	return storeCandidateWithConfig(baseURL, specType, credentials, nil)
}

func storeCandidateWithConfig(baseURL, specType string, credentials, config map[string]any) store.RuntimeModelCandidate {
	return store.RuntimeModelCandidate{BaseURL: baseURL, SpecType: specType, Provider: specType, Credentials: credentials, PlatformConfig: config}
}

func toJSONForTest(value any) string {
	raw, _ := json.Marshal(value)
	return string(raw)
}

func containsStringForTest(values []string, expected string) bool {
	for _, value := range values {
		if value == expected {
			return true
		}
	}
	return false
}
