package services

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

	openai "github.com/sashabaranov/go-openai"
	"wa-assistant/backend/models"
)

func TestVisionRoutesDeepSeekAndAdditionalProviderWithoutCredentialMixing(t *testing.T) {
	db := agenticTestDB(t)
	if err := db.AutoMigrate(&models.AppSetting{}, &models.Agent{}, &models.Product{}); err != nil {
		t.Fatal(err)
	}
	t.Setenv("OPENROUTER_API_KEY", "")
	t.Setenv("DEEPSEEK_API_KEY", "")
	oldTransport := http.DefaultTransport
	aiClientMu.Lock()
	oldCache := aiClientCache
	aiClientCache = make(map[string]*openai.Client)
	aiClientMu.Unlock()
	// A fresh cache ensures fixture requests cannot reuse a live provider transport.
	resetCache := func() { aiClientMu.Lock(); clear(aiClientCache); aiClientMu.Unlock() }
	t.Cleanup(func() {
		http.DefaultTransport = oldTransport
		aiClientMu.Lock()
		aiClientCache = oldCache
		aiClientMu.Unlock()
	})
	cases := []struct {
		name, provider, model, dsKey, orKey, visionModel, wantHost, wantKey, wantModel string
		wantError                                                                      bool
	}{
		{"deepseek vision first", "deepseek", "deepseek-flash", "ds-fixture", "or-fixture", "vendor/vision", "api.deepseek.com", "ds-fixture", "deepseek-flash", false},
		{"deepseek without additional key", "deepseek", "deepseek-flash", "ds-fixture", "", "", "api.deepseek.com", "ds-fixture", "deepseek-flash", false},
		{"text model uses additional", "deepseek", "deepseek-v4-pro", "ds-fixture", "or-fixture", "vendor/vision", "openrouter.ai", "or-fixture", "vendor/vision", false},
		{"unrecognized model does not infer image capability", "deepseek", "deepseek-imaginary-vision", "ds-fixture", "or-fixture", "vendor/vision", "openrouter.ai", "or-fixture", "vendor/vision", false},
		{"openrouter ignores inactive deepseek", "openrouter", "deepseek-flash", "ds-fixture", "or-fixture", "vendor/vision", "openrouter.ai", "or-fixture", "vendor/vision", false},
		{"missing configuration", "deepseek", "deepseek-v4-pro", "ds-fixture", "", "", "", "", "", true},
		{"missing primary key does not silently switch", "deepseek", "deepseek-flash", "", "or-fixture", "vendor/vision", "", "", "", true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			resetCache()
			for key, value := range map[string]string{"ai_provider": tc.provider, "deepseek_model": tc.model, "deepseek_api_key": tc.dsKey, "api_key": tc.orKey, "vision_model": tc.visionModel} {
				if err := db.Save(&models.AppSetting{Key: key, Value: value}).Error; err != nil {
					t.Fatal(err)
				}
			}
			calls := 0
			http.DefaultTransport = providerRoundTrip(func(req *http.Request) (*http.Response, error) {
				calls++
				if req.URL.Host != tc.wantHost || req.Header.Get("Authorization") != "Bearer "+tc.wantKey {
					t.Fatal("wrong provider or credential")
				}
				var body map[string]any
				if json.NewDecoder(req.Body).Decode(&body) != nil || body["model"] != tc.wantModel {
					t.Fatal("wrong request model")
				}
				messages := body["messages"].([]any)
				last := messages[len(messages)-1].(map[string]any)
				parts := last["content"].([]any)
				image := parts[1].(map[string]any)["image_url"].(map[string]any)["url"].(string)
				if last["role"] != "user" || !strings.HasPrefix(image, "data:image/png;base64,") {
					t.Fatal("image payload absent from user message")
				}
				if tc.provider == "deepseek" && tc.model == "deepseek-flash" && body["thinking"].(map[string]any)["type"] != "disabled" {
					t.Fatal("bounded extraction must use non-thinking transport")
				}
				result := `{"analysis":"Terlihat kotak merah.","reply":"Terlihat kotak merah, kak.","answer":"merah","confidence":0.9,"needs_human":false}`
				payload, _ := json.Marshal(map[string]any{"choices": []any{map[string]any{"message": map[string]string{"role": "assistant", "content": result}}}})
				return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(string(payload)))}, nil
			})
			result, err := AnalyzeCustomerImage(1, "Asisten toko", "ramah", "Warna apa?", "", "image/png", []byte("fixture-image"), nil)
			if tc.wantError {
				if err == nil || calls != 0 {
					t.Fatal("unconfigured vision must fail before any provider call")
				}
				return
			}
			if err != nil || calls != 1 || result.Model != tc.wantModel || result.Answer != "merah" {
				t.Fatalf("vision routing failed: calls=%d err=%v", calls, err)
			}
		})
	}
	t.Run("provider rejection does not leak or retry another provider", func(t *testing.T) {
		resetCache()
		for key, value := range map[string]string{"ai_provider": "deepseek", "deepseek_model": "deepseek-flash", "deepseek_api_key": "ds-fixture", "api_key": "or-fixture", "vision_model": "vendor/vision"} {
			db.Save(&models.AppSetting{Key: key, Value: value})
		}
		calls := 0
		http.DefaultTransport = providerRoundTrip(func(req *http.Request) (*http.Response, error) {
			calls++
			if req.URL.Host != "api.deepseek.com" {
				t.Fatal("unexpected fallback provider")
			}
			return &http.Response{StatusCode: 401, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{"error":{"message":"ds-fixture private customer content","type":"authentication_error"}}`))}, nil
		})
		_, err := AnalyzeCustomerImage(1, "Asisten toko", "ramah", "Warna apa?", "", "image/png", []byte("fixture-image"), nil)
		if err == nil || calls != 1 || strings.Contains(err.Error(), "ds-fixture") || strings.Contains(err.Error(), "private customer") {
			t.Fatal("provider failure leaked content or repeated the request")
		}
	})
}

func TestVisionHistoryPreservesScopedObservationsWithoutTreatingThemAsBusinessFacts(t *testing.T) {
	db := agenticTestDB(t)
	now := time.Now()
	rows := []models.ChatHistory{
		{AgentID: 1, Sender: "a", MediaType: "image", ImageAnalysisStatus: "completed", ImageAnalysis: "Kotak merah terlihat.", ImageAnalysisModel: "deepseek-flash", CreatedAt: now},
		{AgentID: 1, Sender: "a", Message: "Foto gagal", MediaType: "image", ImageAnalysisStatus: "failed", ImageAnalysis: "RAHASIA_ERROR", CreatedAt: now.Add(time.Second)},
		{AgentID: 2, Sender: "a", MediaType: "image", ImageAnalysisStatus: "completed", ImageAnalysis: "RAHASIA_TENANT", CreatedAt: now},
		{AgentID: 1, Sender: "b", MediaType: "image", ImageAnalysisStatus: "completed", ImageAnalysis: "RAHASIA_CHAT", CreatedAt: now},
		{AgentID: 1, Sender: "a", MediaType: "image", ImageAnalysisStatus: "completed", ImageAnalysis: "RAHASIA_REVOKED", Revoked: true, CreatedAt: now},
	}
	if err := db.Create(&rows).Error; err != nil {
		t.Fatal(err)
	}
	got, err := LoadRecentChatContext(context.Background(), db, 1, "a", nil)
	if err != nil {
		t.Fatal(err)
	}
	var text strings.Builder
	for _, row := range got {
		text.WriteString(ContextMessageText(row))
	}
	if !strings.Contains(text.String(), "Kotak merah terlihat") || !strings.Contains(text.String(), "bukan bukti bisnis") || strings.Contains(text.String(), "RAHASIA") {
		t.Fatal("image observations were lost, unscoped, or treated as trusted facts")
	}
	huge := SelectChatContext([]models.ChatHistory{{MediaType: "image", ImageAnalysisStatus: "completed", ImageAnalysis: strings.Repeat("🙂", 5000)}}, 100)
	if len(huge) != 1 || len(ContextMessageText(huge[0]))+len(huge[0].Reply)+32 > 100 {
		t.Fatal("image context exceeds shared budget")
	}
}
