package collector

import (
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// realFailingEnvelope 是 2026-08-26 從 youlin 實例抄回來的**原文**：
// HTTP 200、外層 success=true，而工作流其實一個字都沒寫進 KBDB。
// 這一份不是虛構的樣本，是這一票的物證——改動這裡等於改動證據。
const realFailingEnvelope = `{"success":true,"data":{"success":false,"status":500,` +
	`"error":"{\"success\":false,\"error\":\"unreachable\"}"},"duration_ms":2476}`

func TestWebhookFailure_實測那份回應必須被判成失敗(t *testing.T) {
	msg := webhookFailure(realFailingEnvelope)
	if msg == "" {
		t.Fatal("外層 success=true、內層 success=false ⇒ 必須判失敗，否則就是 2026-08-26 那個假綠")
	}
	if !strings.Contains(msg, "沒有真的寫進去") {
		t.Errorf("訊息要講出「沒寫進去」這件事：%q", msg)
	}
	// 使用者看得到這句話 ⇒ 不准出現狀態碼與上游 JSON 原文（同 direct_quota_test.go 的禁字表）
	for _, bad := range []string{"500", "HTTP", "unreachable", "{"} {
		if strings.Contains(msg, bad) {
			t.Errorf("訊息裸露了技術細節 %q：%q", bad, msg)
		}
	}
}

func TestWebhookFailure_只在看得懂的時候才判失敗(t *testing.T) {
	cases := []struct {
		name string
		body string
		fail bool
	}{
		{"一切正常", `{"success":true,"data":{"success":true,"ok":1},"duration_ms":9}`, false},
		{"外層就說失敗", `{"success":false,"error":"workflow not found"}`, true},
		{"內層說失敗", realFailingEnvelope, true},
		{"data 沒有 success 欄位＝看不出來", `{"success":true,"data":{"written":3}}`, false},
		{"data 是陣列", `{"success":true,"data":[1,2,3]}`, false},
		{"data 是字串", `{"success":true,"data":"done"}`, false},
		{"data 是 null", `{"success":true,"data":null}`, false},
		{"根本不是 JSON", `OK`, false},
		{"空回應", ``, false},
		{"被截斷的 JSON", `{"success":true,"data":{"success":false,"err`, false},
	}
	for _, c := range cases {
		got := webhookFailure(c.body) != ""
		if got != c.fail {
			t.Errorf("%s：判失敗=%v，預期 %v（body=%s）", c.name, got, c.fail, c.body)
		}
	}
}

// 🔴 這支是本票的核心迴歸閘：**「已送達」這個章，只能在東西真的寫進去時才准蓋。**
// 沒有它，同一個假綠會以任何一種新的包裝再回來一次
//（2026-08-26 實錄：26 份蓋章、雲端 4 份，而畫面全綠）。
func TestWiring_雲端說沒寫進去就不准蓋已送達的章(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, "報銷規則.md"), []byte("# 報銷規則\n\n內容"), 0o644); err != nil {
		t.Fatal(err)
	}
	var triggers int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasSuffix(r.URL.Path, "/portal/daemon/folder-tree") {
			_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
			return
		}
		_, _ = io.ReadAll(r.Body)
		triggers++
		// 就是實例當天回的那一份：200 ＋ 外層綠、內層紅
		w.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(w, realFailingEnvelope)
	}))
	defer srv.Close()
	defer gemmaCardStub(t, cardFixture("報銷規則", "財務"))()

	manifestPath := filepath.Join(t.TempDir(), "m.json")
	cfg := &DirectConfig{
		WatchFolders: []string{root},
		Manifest:     manifestPath,
		CypherURL:    srv.URL, Namespace: "demo", APIKey: "demo",
		Library: "kb", Extractor: "gemma", ExtractorExplicit: true, GeminiAPIKey: "k-test",
		CardIngestWF: "rag_ingest_card", MaxRemoved: DefaultMaxRemovedRatio,
	}
	results, exit, _ := RunDirectOnce(cfg, false)
	if triggers == 0 {
		t.Fatal("測試沒打到觸發端點，這支測試等於沒驗到東西")
	}
	if exit == 0 {
		t.Errorf("一份都沒真的寫進去，exit 不該是 0：%+v", results)
	}
	_, fileResults := splitInventory(results)
	if len(fileResults) != 1 || fileResults[0].Status != "failed" {
		t.Fatalf("那份檔應該是 failed，不是「已送達」：%+v", fileResults)
	}
	if !strings.Contains(fileResults[0].Error, "沒有真的寫進去") {
		t.Errorf("失敗理由要講人話：%q", fileResults[0].Error)
	}
	// 最關鍵的一格：manifest 不准留下「已送達」的章——留了，content_hash 沒變就永遠不會重送。
	m, err := LoadManifest(manifestPath, root)
	if err != nil {
		t.Fatalf("讀 manifest：%v", err)
	}
	for rel, e := range m.Entries {
		if e.IngestedHash != "" && e.IngestedHash == e.ContentHash {
			t.Errorf("%s 蓋了「已送達」章，但雲端根本沒收到 ⇒ 這份知識會永久消失", rel)
		}
	}
}

// 太大的檔：明知送出去一定失敗，就不要送——而且理由要是人話。
func TestTooBigForWorkersAI_講人話且不送出去(t *testing.T) {
	if why := tooBigForWorkersAI(strings.Repeat("a", 1000), "小檔.md"); why != "" {
		t.Errorf("一般大小的檔不該被擋：%q", why)
	}
	big := strings.Repeat("字", maxWorkersAIExtractBytes) // 中文一字 3 bytes ⇒ 一定超過
	why := tooBigForWorkersAI(big, "mistakes.md")
	if why == "" {
		t.Fatal("超過上限的檔要被擋下")
	}
	if !strings.Contains(why, "萬字") || !strings.Contains(why, "拆成") {
		t.Errorf("要講出多大、以及使用者能做什麼：%q", why)
	}
	for _, bad := range []string{"token", "131000", "HTTP", "8007", "llama"} {
		if strings.Contains(why, bad) {
			t.Errorf("訊息裸露技術細節 %q：%q", bad, why)
		}
	}
}

func TestExtractWithWorkersAI_太大的檔一個請求都不送(t *testing.T) {
	root := t.TempDir()
	big := strings.Repeat("字", maxWorkersAIExtractBytes)
	if err := os.WriteFile(filepath.Join(root, "巨檔.md"), []byte("# 巨檔\n\n"+big), 0o644); err != nil {
		t.Fatal(err)
	}
	var hits int
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hits++
		_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "output": "{}"})
	}))
	defer srv.Close()

	_, err := ExtractWithWorkersAI(srv.URL, "k", root, "巨檔.md", testOrigin())
	if err == nil {
		t.Fatal("太大的檔應該直接失敗")
	}
	if hits != 0 {
		t.Errorf("擋下的檔不該還打雲端一次（燒額度＋佔佇列），實際打了 %d 次", hits)
	}
	if !strings.Contains(err.Error(), "太大") {
		t.Errorf("理由要講人話：%v", err)
	}
}
