package knowledge

import (
	"errors"
	"fmt"
	"strings"
	"testing"
	"unicode/utf8"
)

func TestParse_PlainUTF8(t *testing.T) {
	got, err := Parse("not.txt", "text/plain", []byte("Merhaba dünya.\r\nİkinci satır.\r\n\r\n\r\n\r\nÜçüncü."))
	if err != nil {
		t.Fatal(err)
	}
	want := "Merhaba dünya.\nİkinci satır.\n\nÜçüncü."
	if got != want {
		t.Fatalf("got %q, want %q", got, want)
	}
}

func TestParse_StripsBOMAndControlChars(t *testing.T) {
	got, err := Parse("a.md", "", []byte("\xEF\xBB\xBF# Başlık\x07\x00\u00A0metin"))
	if err == nil {
		// NUL baytı içerdiği için reddedilmeli; kontrol karakteri testini NUL'suz yap.
		t.Fatalf("NUL baytı içeren içerik reddedilmeliydi, çıktı: %q", got)
	}
	got, err = Parse("a.md", "", []byte("\xEF\xBB\xBF# Başlık\x07\u00A0metin"))
	if err != nil {
		t.Fatal(err)
	}
	if got != "# Başlık metin" {
		t.Fatalf("got %q", got)
	}
}

func TestParse_Windows1254(t *testing.T) {
	// "ğüşöçıİĞŞ" Windows-1254 baytları (geçerli UTF-8 DEĞİL)
	raw := []byte{0xF0, 0xFC, 0xFE, 0xF6, 0xE7, 0xFD, 0xDD, 0xD0, 0xDE, ' ', 0x93, 'x', 0x94}
	got, err := Parse("eski.txt", "text/plain", raw)
	if err != nil {
		t.Fatal(err)
	}
	if got != "ğüşöçıİĞŞ “x”" {
		t.Fatalf("got %q", got)
	}
}

func TestParse_HTML(t *testing.T) {
	src := `<html><head><title>Gizli</title><style>p{color:red}</style></head>
<body><h1>Başlık</h1><script>alert('x')</script><!-- yorum -->
<p>Birinci &amp; ikinci</p><p>Üçüncü<br>satır</p></body></html>`
	got, err := Parse("sayfa.html", "text/html", []byte(src))
	if err != nil {
		t.Fatal(err)
	}
	for _, bad := range []string{"alert", "color:red", "yorum", "Gizli", "<p>"} {
		if strings.Contains(got, bad) {
			t.Fatalf("%q ayıklanmalıydı: %q", bad, got)
		}
	}
	for _, want := range []string{"Başlık", "Birinci & ikinci", "Üçüncü", "satır"} {
		if !strings.Contains(got, want) {
			t.Fatalf("%q bulunmalıydı: %q", want, got)
		}
	}
	if !strings.Contains(got, "Üçüncü\nsatır") {
		t.Fatalf("<br> satır sonuna dönüşmeliydi: %q", got)
	}
}

func TestParse_Rejects(t *testing.T) {
	cases := map[string]struct {
		name, ct string
		data     []byte
	}{
		"pdf uzantı":   {"a.pdf", "", []byte("merhaba")},
		"pdf sihirli":  {"a.txt", "", []byte("%PDF-1.7 ...")},
		"docx":         {"a.docx", "", []byte("merhaba")},
		"zip sihirli":  {"a.txt", "", []byte("PK\x03\x04abc")},
		"görsel tür":   {"a", "image/png", []byte("x")},
		"NUL baytı":    {"a.bin", "", []byte("ab\x00cd")},
		"pdf içerikT.": {"a", "application/pdf", []byte("x")},
	}
	for name, c := range cases {
		_, err := Parse(c.name, c.ct, c.data)
		if !errors.Is(err, ErrUnsupportedFormat) {
			t.Errorf("%s: ErrUnsupportedFormat bekleniyordu, geldi %v", name, err)
		}
	}
}

func TestParse_Empty(t *testing.T) {
	for _, in := range []string{"", "   \n\t \r\n  ", "<html><body>  </body></html>"} {
		name := "a.txt"
		if strings.HasPrefix(in, "<") {
			name = "a.html"
		}
		if _, err := Parse(name, "", []byte(in)); !errors.Is(err, ErrEmptyText) {
			t.Errorf("%q: ErrEmptyText bekleniyordu, geldi %v", in, err)
		}
	}
}

// ---- chunker ----

func words(n int) []string {
	out := make([]string, n)
	for i := range out {
		out[i] = fmt.Sprintf("kelime%04d", i)
	}
	return out
}

func TestSplit_EmptyAndShort(t *testing.T) {
	if got := Split("  \n ", ChunkOptions{}); got != nil {
		t.Fatalf("boş metin nil olmalı: %v", got)
	}
	got := Split("Kısa bir metin.", ChunkOptions{})
	if len(got) != 1 || got[0] != "Kısa bir metin." {
		t.Fatalf("got %q", got)
	}
}

func TestSplit_SizeInvariantAndNoContentLost(t *testing.T) {
	var b strings.Builder
	for i := 0; i < 400; i++ {
		fmt.Fprintf(&b, "Bu %d numaralı cümle şunu anlatır: öğrenilmiş ğüşiöç bilgiler. ", i)
		if i%7 == 6 {
			b.WriteString("\n\n")
		}
	}
	text := b.String()
	for _, o := range []ChunkOptions{{Size: 200, Overlap: 0}, {Size: 300, Overlap: 60}, {Size: 800, Overlap: 100}} {
		chunks := Split(text, o)
		if len(chunks) < 2 {
			t.Fatalf("%+v: birden çok parça beklenirdi", o)
		}
		for i, c := range chunks {
			if c == "" || strings.TrimSpace(c) != c {
				t.Fatalf("%+v: parça %d boş ya da kırpılmamış: %q", o, i, c)
			}
			if n := utf8.RuneCountInString(c); n > o.normalized().Size {
				t.Fatalf("%+v: parça %d %d rune (> %d)", o, i, n, o.Size)
			}
		}
		joined := strings.Join(chunks, "\n")
		for i := 0; i < 400; i++ {
			s := fmt.Sprintf("Bu %d numaralı cümle", i)
			if !strings.Contains(joined, s) {
				t.Fatalf("%+v: %q kayboldu", o, s)
			}
		}
	}
}

func TestSplit_OverlapAndNoLossOnWords(t *testing.T) {
	ws := words(600)
	text := strings.Join(ws, " ") // noktalamasız tek dev birim → kelime sınırında kesilir
	chunks := Split(text, ChunkOptions{Size: 200, Overlap: 50})
	if len(chunks) < 3 {
		t.Fatalf("beklenen çok parça, gelen %d", len(chunks))
	}
	seen := map[string]bool{}
	for i, c := range chunks {
		if n := utf8.RuneCountInString(c); n > 200 {
			t.Fatalf("parça %d: %d rune", i, n)
		}
		for _, w := range strings.Fields(c) {
			if !strings.HasPrefix(w, "kelime") || len(w) != 10 {
				t.Fatalf("parça %d kelime ortasında kesildi: %q", i, w)
			}
			seen[w] = true
		}
		if i > 0 {
			first := strings.Fields(c)[0]
			if !strings.Contains(chunks[i-1], first) {
				t.Fatalf("parça %d örtüşmeyle başlamıyor (%q önceki parçada yok)", i, first)
			}
		}
	}
	for _, w := range ws {
		if !seen[w] {
			t.Fatalf("%s kayboldu", w)
		}
	}
}

func TestSplit_NoOverlapMeansNoDuplicates(t *testing.T) {
	ws := words(500)
	chunks := Split(strings.Join(ws, " "), ChunkOptions{Size: 200, Overlap: 0})
	count := map[string]int{}
	for _, c := range chunks {
		for _, w := range strings.Fields(c) {
			count[w]++
		}
	}
	for _, w := range ws {
		if count[w] != 1 {
			t.Fatalf("%s %d kez geçiyor", w, count[w])
		}
	}
}

func TestSplit_PrefersParagraphBoundaries(t *testing.T) {
	p1 := strings.Repeat("a", 150) + " birinci paragraf"
	p2 := strings.Repeat("b", 150) + " ikinci paragraf"
	text := p1 + "\n\n" + p2

	one := Split(text, ChunkOptions{Size: 800})
	if len(one) != 1 || !strings.Contains(one[0], "\n\n") {
		t.Fatalf("sığıyorsa tek parça ve paragraf ayrımı korunmalı: %q", one)
	}
	two := Split(text, ChunkOptions{Size: 200, Overlap: 0})
	if len(two) != 2 || two[0] != p1 || two[1] != p2 {
		t.Fatalf("paragraf sınırında bölünmeliydi: %q", two)
	}
}

func TestSplit_HugeSingleWord(t *testing.T) {
	text := strings.Repeat("ş", 2500)
	chunks := Split(text, ChunkOptions{Size: 1000, Overlap: 0})
	total := 0
	for _, c := range chunks {
		n := utf8.RuneCountInString(c)
		if n > 1000 {
			t.Fatalf("%d rune > 1000", n)
		}
		total += n
	}
	if total != 2500 {
		t.Fatalf("toplam %d rune, 2500 beklenirdi", total)
	}
}

func TestSplit_Deterministic(t *testing.T) {
	text := strings.Repeat("Türkçe bir cümle burada. Başka bir cümle orada! Soru mu? ", 80)
	a := Split(text, ChunkOptions{Size: 250, Overlap: 40})
	b := Split(text, ChunkOptions{Size: 250, Overlap: 40})
	if fmt.Sprint(a) != fmt.Sprint(b) {
		t.Fatal("aynı girdi farklı parçalar üretti")
	}
}

func TestSplitSentences_DoesNotBreakNumbers(t *testing.T) {
	got := splitSentences("Pi sayısı 3.14 olarak bilinir. İkinci cümle? Evet!\nYeni satır")
	var texts []string
	for _, u := range got {
		texts = append(texts, u.text)
	}
	want := []string{"Pi sayısı 3.14 olarak bilinir.", "İkinci cümle?", "Evet!", "Yeni satır"}
	if fmt.Sprint(texts) != fmt.Sprint(want) {
		t.Fatalf("got %q, want %q", texts, want)
	}
}

func TestChunkOptions_Normalized(t *testing.T) {
	o := ChunkOptions{Size: 10, Overlap: 9999}.normalized()
	if o.Size != MinChunkSize || o.Overlap != MinChunkSize/2 {
		t.Fatalf("got %+v", o)
	}
	if o := (ChunkOptions{Size: 99999}).normalized(); o.Size != MaxChunkSize {
		t.Fatalf("got %+v", o)
	}
	if o := (ChunkOptions{}).normalized(); o.Size != DefaultChunkSize {
		t.Fatalf("got %+v", o)
	}
}
