package export

import (
	"regexp"
	"strings"
	"testing"
)

func deterministicRand(t *testing.T) {
	t.Helper()
	old := randIntN
	randIntN = func(n int) int { return 0 }
	t.Cleanup(func() { randIntN = old })
}

var markerRe = regexp.MustCompile(`\[AD\]|\[AI\]|\[D\]|\[IMG\]|<h2>`)

// events extracts the ordered marker log from rendered body HTML.
func events(html string) []string {
	var out []string
	for _, m := range markerRe.FindAllString(html, -1) {
		switch m {
		case "[AD]":
			out = append(out, "ad")
		case "[AI]":
			out = append(out, "ai")
		case "[D]":
			out = append(out, "desc")
		case "[IMG]":
			out = append(out, "img")
		case "<h2>":
			out = append(out, "h2")
		}
	}
	return out
}

// fixtureRow builds a row with 12 ai blocks, 10 descriptions, 10 images and
// 6 related keywords, all carrying marker text for event extraction.
func fixtureRow(includeAI, includeDesc bool) Article {
	row := Article{Keyword: "kw", Slug: "kw"}
	for i := 0; i < 10; i++ {
		row.Images = append(row.Images, Image{ImageURL: "[IMG]", Title: "t"})
	}
	if includeAI {
		var sb strings.Builder
		for i := 0; i < 12; i++ {
			sb.WriteString("<p>[AI]</p>")
		}
		row.AIContent = sb.String()
	}
	if includeDesc {
		for i := 0; i < 10; i++ {
			row.Snippet.Description = append(row.Snippet.Description, "[D]")
		}
		row.Snippet.RelatedKW = []string{"main kw", "second kw", "third kw", "fourth kw", "fifth kw", "sixth kw"}
	}
	return row
}

func wantSeq(t *testing.T, got []string, want []string) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("event count = %d (%v), want %d (%v)", len(got), got, len(want), want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("event[%d] = %s, want %s (all: %v)", i, got[i], want[i], got)
		}
	}
}

func repeat(s string, n int) []string {
	out := make([]string, n)
	for i := range out {
		out[i] = s
	}
	return out
}

func concat(parts ...[]string) []string {
	var out []string
	for _, p := range parts {
		out = append(out, p...)
	}
	return out
}

// Case A: 12 ai blocks (3+3+3+3 placed, slot of 4 takes remainder), 10 images
// (1+2+1+2+1+3), 10 descriptions (2+4+4), 3 H2, 7 ads.
func TestBuildBodyCaseA(t *testing.T) {
	deterministicRand(t)
	html := BuildBody(fixtureRow(true, true), "modern", "[AD]")
	seq := concat(
		[]string{"ad"}, repeat("ai", 3),
		[]string{"ad"}, repeat("img", 1),
		repeat("ai", 3),
		[]string{"ad"}, repeat("img", 2),
		repeat("ai", 3),
		repeat("img", 1),
		[]string{"ad"}, repeat("ai", 3), // slot of 4 takes the 3 remaining
		[]string{"ad"},
		repeat("img", 2),
		// rest ai: none left
		[]string{"h2"}, repeat("desc", 2),
		[]string{"img"},
		[]string{"h2"}, repeat("desc", 4),
		[]string{"ad"},
		repeat("img", 3), // rest images
		[]string{"h2"}, repeat("desc", 4), // rest descriptions
		[]string{"ad"},
	)
	wantSeq(t, events(html), seq)
}

// Case B: 10 descriptions (3+3+3+1, slots later take 0), 10 images (1+2+1+6),
// 4 H2, 6 ads.
func TestBuildBodyCaseB(t *testing.T) {
	deterministicRand(t)
	html := BuildBody(fixtureRow(false, true), "modern", "[AD]")
	seq := concat(
		[]string{"ad"}, repeat("desc", 3),
		[]string{"ad"}, repeat("img", 1),
		[]string{"h2"}, repeat("desc", 3),
		[]string{"ad"}, repeat("img", 2),
		[]string{"h2"}, repeat("desc", 3),
		[]string{"img", "ad", "h2"},
		repeat("desc", 1), // slot of 4 takes the 1 remaining
		[]string{"ad"},
		// slot of 2 desc takes 0
		repeat("img", 6), // rest images
		[]string{"h2"},
		// rest desc: none left
		[]string{"ad"},
	)
	wantSeq(t, events(html), seq)
}

// Case C: 12 ai blocks (3+3+3+3), 10 images (1+2+1+6), 5 ads, no H2/desc.
func TestBuildBodyCaseC(t *testing.T) {
	deterministicRand(t)
	html := BuildBody(fixtureRow(true, false), "modern", "[AD]")
	seq := concat(
		[]string{"ad"}, repeat("ai", 3),
		[]string{"ad"}, repeat("img", 1),
		repeat("ai", 3),
		[]string{"ad"}, repeat("img", 2),
		repeat("ai", 3),
		repeat("img", 1),
		[]string{"ad"}, repeat("ai", 3),
		[]string{"ad"},
		repeat("img", 6), // rest images
		// rest ai: none left
		[]string{"ad"},
	)
	wantSeq(t, events(html), seq)
}

func TestBuildBodyPartialItems(t *testing.T) {
	deterministicRand(t)
	// Fewer items than slots: no panic, everything placed exactly once.
	row := Article{
		Keyword:   "kw",
		Images:    []Image{{ImageURL: "[IMG]", Title: "t"}},
		AIContent: "<p>[AI]</p>",
		Snippet: SnippetData{
			Description: []string{"[D]"},
			RelatedKW:   []string{"main kw"},
		},
	}
	html := BuildBody(row, "modern", "[AD]")
	// Case A slots with 1 ai, 1 img, 1 desc and no usable H2 (only 1 related).
	wantSeq(t, events(html), []string{"ad", "ai", "ad", "img", "ad", "ad", "ad", "desc", "ad", "ad"})
}

func TestBuildBodyNoRelatedNoH2(t *testing.T) {
	deterministicRand(t)
	row := fixtureRow(false, true)
	row.Snippet.RelatedKW = nil
	html := BuildBody(row, "modern", "[AD]")
	for _, ev := range events(html) {
		if ev == "h2" {
			t.Fatal("h2 must be skipped when related_kw is empty")
		}
	}
}

func TestBuildBodyNoAds(t *testing.T) {
	deterministicRand(t)
	html := BuildBody(fixtureRow(true, true), "modern", "")
	for _, ev := range events(html) {
		if ev == "ad" {
			t.Fatal("ads must be skipped when inject is empty")
		}
	}
}

func TestBuildBodyH2SkipsFirstRelated(t *testing.T) {
	deterministicRand(t)
	html := BuildBody(fixtureRow(false, true), "modern", "")
	if strings.Contains(html, "Main Kw") {
		t.Fatal("first related_kw must never be used as H2")
	}
	if !strings.Contains(html, "<h2>Second Kw</h2>") {
		t.Fatalf("expected second related as H2, got: %.400s", html)
	}
}
