package backend

import (
	"encoding/json"
	"strings"
	"testing"
)

func TestPrepareImageExtractWritebackStructuredJSON(t *testing.T) {
	raw := `{
  "caption_zh": "秋季林荫道路",
  "description_zh": "一条蜿蜒柏油路穿过金黄色树林，阳光穿过树冠形成斑驳光影。",
  "caption_en": "Autumn tree-lined road",
  "description_en": "A winding asphalt road runs through tall trees with golden leaves and warm sunlight.",
  "ocr_text": [],
  "tags_zh": ["秋季", "林荫路", "树木", "金黄色", "暖色调", "照片"],
  "tags_en": ["Autumn", "tree-lined road", "golden leaves", "warm light", "photo", "ocr"],
  "search_queries_zh": ["秋天金黄色树林道路", "暖色调林荫路风景"],
  "search_queries_en": ["autumn golden tree road", "warm tree-lined road landscape"],
  "attributes": {"season": "autumn", "scene": "tree-lined road", "color": "golden"}
}`

	writeback := prepareImageExtractWriteback(raw, 32<<10)
	if writeback.text == "" {
		t.Fatal("text is empty")
	}
	if strings.Contains(writeback.text, "caption_zh") || strings.Contains(writeback.text, "{") {
		t.Fatalf("text should be canonical text, got %q", writeback.text)
	}
	if !strings.Contains(writeback.text, "秋季林荫道路\n中文描述") {
		t.Fatalf("structured text should preserve line breaks, got %q", writeback.text)
	}
	for _, want := range []string{
		"中文摘要：秋季林荫道路",
		"中文描述：一条蜿蜒柏油路穿过金黄色树林",
		"英文摘要：Autumn tree-lined road",
		"英文标签：autumn, tree-lined road, golden leaves, warm light",
		"中文搜索短语：秋天金黄色树林道路",
		"属性：color=golden，scene=tree-lined road，season=autumn",
	} {
		if !strings.Contains(writeback.text, want) {
			t.Fatalf("text %q does not contain %q", writeback.text, want)
		}
	}
	wantTags := map[string]string{"season": "autumn", "scene": "tree-lined road", "color": "golden"}
	if len(writeback.tags) != len(wantTags) {
		t.Fatalf("tags=%+v, want %+v", writeback.tags, wantTags)
	}
	for k, v := range wantTags {
		if writeback.tags[k] != v {
			t.Fatalf("tag %q=%q, want %q (tags=%+v)", k, writeback.tags[k], v, writeback.tags)
		}
	}
}

func TestOpenAIImageTextExtractorStructuredDefaults(t *testing.T) {
	extractor, err := NewOpenAIImageTextExtractor(OpenAIImageTextExtractorConfig{
		BaseURL: "https://example.com/v1",
		APIKey:  "secret",
		Model:   "vision-model",
		Prompt:  ComposeImageExtractPrompt(""),
	})
	if err != nil {
		t.Fatal(err)
	}
	if extractor.maxTokens != DefaultOpenAIImageExtractMaxTokens {
		t.Fatalf("maxTokens=%d, want %d", extractor.maxTokens, DefaultOpenAIImageExtractMaxTokens)
	}
	if !strings.Contains(extractor.prompt, `"caption_zh"`) || !strings.Contains(extractor.prompt, `"tags_en"`) || !strings.Contains(extractor.prompt, `"attributes"`) {
		t.Fatalf("default prompt is not structured JSON prompt: %q", extractor.prompt)
	}
	if !strings.HasSuffix(extractor.prompt, imageAttributesSuffix) {
		t.Fatalf("default prompt missing attributes suffix: %q", extractor.prompt)
	}
}

func TestOpenAIImageTextExtractorLeavesEmptyPromptToCaller(t *testing.T) {
	extractor, err := NewOpenAIImageTextExtractor(OpenAIImageTextExtractorConfig{
		BaseURL: "https://example.com/v1",
		APIKey:  "secret",
		Model:   "vision-model",
	})
	if err != nil {
		t.Fatal(err)
	}
	if extractor.prompt != "" {
		t.Fatalf("prompt=%q, want empty (composition is the caller's job)", extractor.prompt)
	}
}

func TestPrepareImageExtractWritebackParsesMarkdownFencedJSON(t *testing.T) {
	raw := "```json\n" + `{
  "caption_zh": "白板会议",
  "description_zh": "办公室里有人在白板前讨论。",
  "caption_en": "Whiteboard meeting",
  "description_en": "People discuss ideas in front of a whiteboard.",
  "ocr_text": "Q2 Roadmap",
  "tags_zh": ["白板", "会议"],
  "tags_en": ["whiteboard", "meeting"],
  "search_queries_zh": [],
  "search_queries_en": [],
  "attributes": {"subject": "whiteboard", "activity": "meeting"}
}` + "\n```"

	writeback := prepareImageExtractWriteback(raw, 32<<10)
	if !strings.Contains(writeback.text, "图中文字：Q2 Roadmap") {
		t.Fatalf("expected OCR text in canonical output, got %q", writeback.text)
	}
	if writeback.tags["subject"] != "whiteboard" || writeback.tags["activity"] != "meeting" {
		t.Fatalf("attributes should become top-level tags, got %+v", writeback.tags)
	}
	if !strings.Contains(writeback.text, "属性：activity=meeting，subject=whiteboard") {
		t.Fatalf("attribute values should remain in text, got %q", writeback.text)
	}
}

func TestPrepareImageExtractWritebackFallbackRawText(t *testing.T) {
	raw := "cat on sofa screenshot invoice"
	writeback := prepareImageExtractWriteback(raw, 32<<10)
	if writeback.text != raw {
		t.Fatalf("text=%q, want %q", writeback.text, raw)
	}
	if len(writeback.tags) != 0 {
		t.Fatalf("fallback tags=%+v, want empty", writeback.tags)
	}
}

func TestPrepareImageExtractWritebackAppliesByteLimit(t *testing.T) {
	raw := `{
  "caption_zh": "很长的描述",
  "description_zh": "` + strings.Repeat("秋", 200) + `",
  "caption_en": "",
  "description_en": "",
  "ocr_text": [],
  "tags_zh": ["秋季"],
  "tags_en": ["autumn"],
  "search_queries_zh": [],
  "search_queries_en": [],
  "attributes": {"season": "秋季"}
}`

	writeback := prepareImageExtractWriteback(raw, 64)
	if len(writeback.text) > 64 {
		t.Fatalf("text length=%d, want <=64", len(writeback.text))
	}
	if writeback.tags["season"] != "秋季" {
		t.Fatalf("tags should still be generated after text truncation, got %+v", writeback.tags)
	}
}

func TestTruncateByEstimatedTokens(t *testing.T) {
	text := strings.Repeat("秋", 9000)
	got := truncateByEstimatedTokens(text, 8000)
	if estimateImageExtractTokens(got) > 8000 {
		t.Fatalf("estimated tokens=%d, want <=8000", estimateImageExtractTokens(got))
	}
	if len([]rune(got)) != 8000 {
		t.Fatalf("rune count=%d, want 8000", len([]rune(got)))
	}
}

func TestPrepareImageExtractWritebackAttributesOnlyResponse(t *testing.T) {
	raw := `{"attributes": {"style": "portrait", "scene": "outdoor"}}`
	writeback := prepareImageExtractWriteback(raw, 32<<10)
	if len(writeback.tags) != 2 || writeback.tags["style"] != "portrait" || writeback.tags["scene"] != "outdoor" {
		t.Fatalf("attributes-only response should parse as structured, tags=%+v", writeback.tags)
	}
	if !strings.Contains(writeback.text, "属性：scene=outdoor，style=portrait") {
		t.Fatalf("attribute values should remain in content_text, got %q", writeback.text)
	}
}

func TestPrepareImageExtractWritebackKeepsStructuredFieldsWhenAttributesContainNonStrings(t *testing.T) {
	raw := `{
  "caption_zh": "年度报告封面",
  "attributes": {
    "year": 2024,
    "published": true,
    "style": "poster",
    "unknown": null,
    "nested": {"kind": "report"},
    "topics": ["finance"]
  }
}`

	writeback := prepareImageExtractWriteback(raw, 32<<10)
	if !strings.Contains(writeback.text, "中文摘要：年度报告封面") {
		t.Fatalf("valid structured fields were dropped: %q", writeback.text)
	}
	wantTags := map[string]string{
		"year":      "2024",
		"published": "true",
		"style":     "poster",
	}
	if len(writeback.tags) != len(wantTags) {
		t.Fatalf("tags = %+v, want %+v", writeback.tags, wantTags)
	}
	for key, want := range wantTags {
		if got := writeback.tags[key]; got != want {
			t.Fatalf("tag %q = %q, want %q", key, got, want)
		}
	}
	for _, dropped := range []string{"unknown", "nested", "topics"} {
		if _, ok := writeback.tags[dropped]; ok {
			t.Fatalf("non-useful attribute %q should be dropped: %+v", dropped, writeback.tags)
		}
	}
}

func TestPrepareImageExtractWritebackDropsInvalidAttributes(t *testing.T) {
	attrs := map[string]string{
		"style":                  "portrait",
		"drive9.reserved":        "x",                      // reserved drive9. prefix
		"bad=key":                "x",                      // '=' in key
		strings.Repeat("k", 65):  "x",                      // key > 64 runes
		"long":                   strings.Repeat("v", 256), // value > 255 runes
		"  padded  ":             "x",                      // uncleanable padding
		"internal \t whitespace": "x",                      // control char in key
	}
	body, err := json.Marshal(map[string]any{
		"caption_zh": "人像照片",
		"attributes": attrs,
	})
	if err != nil {
		t.Fatal(err)
	}
	writeback := prepareImageExtractWriteback(string(body), 32<<10)
	if len(writeback.tags) != 1 || writeback.tags["style"] != "portrait" {
		t.Fatalf("invalid attributes should be dropped, tags=%+v", writeback.tags)
	}
	if strings.Contains(writeback.text, "drive9.reserved") || strings.Contains(writeback.text, "bad=key") {
		t.Fatalf("dropped attributes should not leak into content_text, got %q", writeback.text)
	}
}

func TestPrepareImageExtractWritebackCapsAttributeCount(t *testing.T) {
	attrs := make(map[string]string, 35)
	for i := 0; i < 35; i++ {
		attrs[strings.Repeat("a", i+1)] = "v"
	}
	body, err := json.Marshal(map[string]any{
		"caption_zh": "人像照片",
		"attributes": attrs,
	})
	if err != nil {
		t.Fatal(err)
	}
	writeback := prepareImageExtractWriteback(string(body), 32<<10)
	if len(writeback.tags) != 30 {
		t.Fatalf("attribute count=%d, want capped at 30 (tags=%+v)", len(writeback.tags), writeback.tags)
	}
	// Sorted order is the deterministic truncation order: keys "a".."aaa..." shortest first.
	for i := 0; i < 30; i++ {
		k := strings.Repeat("a", i+1)
		if writeback.tags[k] != "v" {
			t.Fatalf("expected key %q to be kept, tags=%+v", k, writeback.tags)
		}
	}
}
