package sync

import (
	"strings"
	"testing"

	"golang.org/x/net/html"
)

func TestParseCREXPointer(t *testing.T) {
	tests := []struct {
		input   string
		wantFrag int
		wantPath int
		wantChar int
	}{
		{"/body/DocFragment[0]/body/div[4]/p[38]/text().541", 0, 2, 541},
		{"/body/DocFragment[2]/body/div/p[5]/text()[2].16", 2, 2, 16},
		{"/body/DocFragment[1]/body", 1, 0, 0},
		{"/body/DocFragment[5]/body/div[3]/p[28]", 5, 2, 0},
	}

	for _, tt := range tests {
		xp, err := ParseCREXPointer(tt.input)
		if err != nil {
			t.Errorf("ParseCREXPointer(%q) error: %v", tt.input, err)
			continue
		}
		if xp.FragmentIndex != tt.wantFrag {
			t.Errorf("FragmentIndex = %d, want %d", xp.FragmentIndex, tt.wantFrag)
		}
		if len(xp.ElementPath) != tt.wantPath {
			t.Errorf("len(ElementPath) = %d, want %d (got %+v)", len(xp.ElementPath), tt.wantPath, xp.ElementPath)
		}
		if xp.CharOffset != tt.wantChar {
			t.Errorf("CharOffset = %d, want %d", xp.CharOffset, tt.wantChar)
		}
	}
}

func TestParseCREXPointerInvalid(t *testing.T) {
	_, err := ParseCREXPointer("epubcfi(/6/4!/4/2/1:0)")
	if err == nil {
		t.Error("expected error for standard epubcfi")
	}
	_, err = ParseCREXPointer("")
	if err == nil {
		t.Error("expected error for empty string")
	}
}

func TestIsCREXPointer(t *testing.T) {
	if !IsCREXPointer("/body/DocFragment[0]/body/div/p") {
		t.Error("should recognize CRE XPointer")
	}
	if !IsCREXPointer("#_doc_fragment_5_ link2HCH0002") {
		t.Error("should recognize CRE fragment ID")
	}
	if IsCREXPointer("epubcfi(/6/4!/4/2/1:0)") {
		t.Error("should not recognize standard epubcfi as CRE")
	}
}

func TestIsCREFragmentID(t *testing.T) {
	if !IsCREFragmentID("#_doc_fragment_5_ link2HCH0002") {
		t.Error("should recognize fragment ID")
	}
	if IsCREFragmentID("/body/DocFragment[2]/body") {
		t.Error("should not recognize XPointer as fragment ID")
	}
}

func TestParseCREFragmentID(t *testing.T) {
	tests := []struct {
		input       string
		wantSpine   int
		wantAnchor  string
	}{
		{"#_doc_fragment_5_ link2HCH0002", 5, "link2HCH0002"},
		{"#_doc_fragment_0_", 0, ""},
		{"#_doc_fragment_12_someid123", 12, "someid123"},
	}

	for _, tt := range tests {
		frag, err := ParseCREFragmentID(tt.input)
		if err != nil {
			t.Errorf("ParseCREFragmentID(%q) error: %v", tt.input, err)
			continue
		}
		if frag.SpineIndex != tt.wantSpine {
			t.Errorf("SpineIndex = %d, want %d", frag.SpineIndex, tt.wantSpine)
		}
		if frag.Anchor != tt.wantAnchor {
			t.Errorf("Anchor = %q, want %q", frag.Anchor, tt.wantAnchor)
		}
	}
}

func TestParseCREFragmentIDInvalid(t *testing.T) {
	_, err := ParseCREFragmentID("/body/DocFragment[2]/body")
	if err == nil {
		t.Error("expected error for XPointer input")
	}
	_, err = ParseCREFragmentID("#_doc_fragment_")
	if err == nil {
		t.Error("expected error for missing index")
	}
}

func TestIsStandardEPUBCFI(t *testing.T) {
	if !IsStandardEPUBCFI("epubcfi(/6/4!/4/2/1:0)") {
		t.Error("should recognize standard epubcfi")
	}
	if IsStandardEPUBCFI("/body/DocFragment[0]/body") {
		t.Error("should not recognize CRE as standard")
	}
}

func TestConvert1984(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
	c := NewCFIConverter(epubPath)

	xp := "/body/DocFragment[2]/body/div/p[5]/text().500"
	result, err := c.ConvertCREToStandard(xp, 0.01, "")
	if err != nil {
		t.Fatalf("ConvertCREToStandard error: %v", err)
	}
	t.Logf("Input: %s", xp)
	t.Logf("EPUBCFI: %s", result.EPUBCFI)
	t.Logf("Href: %s", result.Href)
	t.Logf("Precision: %s", result.Precision)
	t.Logf("Percentage: %.4f", result.Percentage)

	if result.Precision == "percentage" {
		t.Error("expected better than percentage precision")
	}
}

func TestConvertCrimeAndPunishmentFragmentID(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
	c := NewCFIConverter(epubPath)

	xp := "#_doc_fragment_5_ link2HCH0002"
	result, err := c.ConvertCREToStandard(xp, 0.0303, "")
	if err != nil {
		t.Fatalf("ConvertCREToStandard error: %v", err)
	}
	t.Logf("Input: %s", xp)
	t.Logf("EPUBCFI: %s", result.EPUBCFI)
	t.Logf("Href: %s", result.Href)
	t.Logf("Precision: %s", result.Precision)
	t.Logf("Percentage: %.4f", result.Percentage)

	if result.Precision == "percentage" {
		t.Error("expected better than percentage precision")
	}
	if result.Href == "" {
		t.Error("expected non-empty href")
	}
	if result.Precision != "element" {
		t.Errorf("expected element precision, got %s", result.Precision)
	}
}

func TestParseEPUBCFI(t *testing.T) {
	tests := []struct {
		input      string
		wantSpine  int
		wantSteps  int
	}{
		{"epubcfi(/6/12!/4/2/90/1:7)", 5, 4},
		{"epubcfi(/6/4!/4/2/1:0)", 1, 3},
		{"epubcfi(/6/2!/4)", 0, 1},
	}

	for _, tt := range tests {
		spineIndex, steps, err := parseEPUBCFI(tt.input)
		if err != nil {
			t.Errorf("parseEPUBCFI(%q) error: %v", tt.input, err)
			continue
		}
		if spineIndex != tt.wantSpine {
			t.Errorf("spineIndex = %d, want %d", spineIndex, tt.wantSpine)
		}
		if len(steps) != tt.wantSteps {
			t.Errorf("len(steps) = %d, want %d", len(steps), tt.wantSteps)
		}
	}
}

func TestParseEPUBCFIRange(t *testing.T) {
	spineIndex, steps, err := parseEPUBCFI("epubcfi(/6/40!/4,/24/20,/40/5:61)")
	if err != nil {
		t.Fatalf("parseEPUBCFI range error: %v", err)
	}
	if spineIndex != 19 {
		t.Errorf("spineIndex = %d, want 19", spineIndex)
	}
	t.Logf("Range CFI steps: %d", len(steps))
	for i, s := range steps {
		t.Logf("  step %d: index=%d offset=%d hasOffset=%v", i, s.Index, s.Offset, s.HasOffset)
	}
}

func TestParseEPUBCFIInvalid(t *testing.T) {
	_, _, err := parseEPUBCFI("not-a-cfi")
	if err == nil {
		t.Error("expected error for invalid CFI")
	}
	_, _, err = parseEPUBCFI("epubcfi(/6/12)")
	if err == nil {
		t.Error("expected error for CFI without indirection")
	}
}

func TestRoundTrip1984(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
	c := NewCFIConverter(epubPath)

	originalXP := "/body/DocFragment[2]/body/div/p[5]/text().500"
	forward, err := c.ConvertCREToStandard(originalXP, 0.01, "")
	if err != nil {
		t.Fatalf("forward conversion error: %v", err)
	}
	if forward.EPUBCFI == "" {
		t.Fatal("forward conversion produced empty epubcfi")
	}
	t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)

	reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, "")
	if err != nil {
		t.Fatalf("reverse conversion error: %v", err)
	}
	if reverse.XPointer == "" {
		t.Fatal("reverse conversion produced empty XPointer")
	}
	t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
	t.Logf("Reverse precision: %s", reverse.Precision)

	if reverse.Precision != "exact" {
		t.Errorf("expected exact precision, got %s", reverse.Precision)
	}
}

func TestRoundTripCP(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
	c := NewCFIConverter(epubPath)

	originalXP := "/body/DocFragment[6]/body/div/p[47]/text().2399"
	contextText := "Raskolnikov was not used to crowds, and, as we said before, he avoided society of every sort, more especially of l"
	forward, err := c.ConvertCREToStandard(originalXP, 0.0579, contextText)
	if err != nil {
		t.Fatalf("forward conversion error: %v", err)
	}
	if forward.EPUBCFI == "" {
		t.Fatal("forward conversion produced empty epubcfi")
	}
	t.Logf("Forward: %s → %s", originalXP, forward.EPUBCFI)

	reverse, err := c.ConvertStandardToCRE(forward.EPUBCFI, forward.Percentage, contextText)
	if err != nil {
		t.Fatalf("reverse conversion error: %v", err)
	}
	if reverse.XPointer == "" {
		t.Fatal("reverse conversion produced empty XPointer")
	}
	t.Logf("Reverse: %s → %s", forward.EPUBCFI, reverse.XPointer)
	t.Logf("Reverse precision: %s", reverse.Precision)

	if reverse.Precision != "exact" {
		t.Errorf("expected exact precision, got %s", reverse.Precision)
	}
}

func TestReverseTextSearchFallback(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/Fyodor Dostoyevsky/Crime and Punishment (103)/Crime and Punishment - Fyodor Dostoyevsky.epub"
	c := NewCFIConverter(epubPath)

	contextText := "Raskolnikov was not used to crowds, and, as we said before, he avoided society of every sort, more especially of l"
	reverse, err := c.ConvertStandardToCRE("epubcfi(/6/12!/4/99999/1:0)", 0.0579, contextText)
	if err != nil {
		t.Fatalf("reverse conversion error: %v", err)
	}
	t.Logf("Text search fallback XPointer: %s", reverse.XPointer)
	t.Logf("Precision: %s", reverse.Precision)

	if reverse.Precision != "exact" {
		t.Errorf("expected exact precision from text search, got %s", reverse.Precision)
	}
	if reverse.XPointer == "" {
		t.Error("expected non-empty XPointer from text search")
	}
}

func TestReversePercentageFallback(t *testing.T) {
	epubPath := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (126)/1984 - George Orwell.epub"
	c := NewCFIConverter(epubPath)

	reverse, err := c.ConvertStandardToCRE("epubcfi(/6/12!/4/99999/1:0)", 0.5, "")
	if err != nil {
		t.Fatalf("reverse conversion error: %v", err)
	}
	t.Logf("Percentage fallback precision: %s", reverse.Precision)

	if reverse.Precision != "percentage" {
		t.Errorf("expected percentage precision, got %s with XPointer %s", reverse.Precision, reverse.XPointer)
	}
	if reverse.XPointer != "" {
		t.Error("expected empty XPointer for percentage fallback")
	}
}

func TestFindTextInNode_SingleTextNode(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>Hello world this is a test</p></body></html>`)
	body := findBody(doc)

	node, offset := findTextInNode(body, "Hello world")
	if node == nil {
		t.Fatal("expected to find text")
	}
	if offset != 0 {
		t.Errorf("offset = %d, want 0", offset)
	}
}

func TestFindTextInNode_CrossEmElement(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>the countries <em>Vokalia</em> and <em>Consonantia</em> live here</p></body></html>`)
	body := findBody(doc)

	node, offset := findTextInNode(body, "Vokalia and Consonantia")
	if node == nil {
		t.Fatal("expected to find text across <em> elements")
	}
	if node.Data != "Vokalia" {
		t.Errorf("expected match in 'Vokalia' text node, got %q", node.Data)
	}
	if offset != 0 {
		t.Errorf("offset = %d, want 0", offset)
	}
}

func TestFindTextInNode_CrossStrongElement(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>Some <strong>bold and italic</strong> text here</p></body></html>`)
	body := findBody(doc)

	node, _ := findTextInNode(body, "bold and italic text")
	if node == nil {
		t.Fatal("expected to find text across <strong> boundary")
	}
	if node.Data != "bold and italic" {
		t.Errorf("expected match in 'bold and italic' text node, got %q", node.Data)
	}
}

func TestFindTextInNode_DoesNotCrossParagraphs(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>first paragraph</p><p>second paragraph</p></body></html>`)
	body := findBody(doc)

	node, _ := findTextInNode(body, "paragraph second")
	if node != nil {
		t.Error("should not match text across <p> boundaries")
	}
}

func TestFindTextInNode_NestedFormatting(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>before <em><strong>bold italic</strong></em> after</p></body></html>`)
	body := findBody(doc)

	node, offset := findTextInNode(body, "bold italic after")
	if node == nil {
		t.Fatal("expected to find text across nested formatting")
	}
	if node.Data != "bold italic" {
		t.Errorf("expected match in 'bold italic' text node, got %q", node.Data)
	}
	_ = offset
}

func TestFindBlockParent(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>text <em>inside <strong>deep</strong></em></p></body></html>`)
	body := findBody(doc)

	var deepNode *html.Node
	var walk func(*html.Node)
	walk = func(n *html.Node) {
		if n.Type == html.TextNode && n.Data == "deep" {
			deepNode = n
			return
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			walk(c)
		}
	}
	walk(body)

	if deepNode == nil {
		t.Fatal("could not find 'deep' text node")
	}

	block := findBlockParent(deepNode)
	if block == nil {
		t.Fatal("expected block parent")
	}
	if block.Data != "p" {
		t.Errorf("block parent = %q, want 'p'", block.Data)
	}
}

func TestCollectInlineText(t *testing.T) {
	doc := parseTestHTML(`<html><body><p>the countries <em>Vokalia</em> and <em>Consonantia</em> live</p></body></html>`)
	body := findBody(doc)

	var p *html.Node
	var walk func(*html.Node)
	walk = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "p" {
			p = n
			return
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			walk(c)
		}
	}
	walk(body)

	if p == nil {
		t.Fatal("could not find <p> element")
	}

	segments := collectInlineText(p)

	var collected []rune
	for _, seg := range segments {
		collected = append(collected, seg.runes...)
	}

	flattened := strings.TrimSpace(string(collected))
	if flattened != "the countries Vokalia and Consonantia live" {
		t.Errorf("collected text = %q", flattened)
	}
}

func parseTestHTML(s string) *html.Node {
	doc, err := html.Parse(strings.NewReader(s))
	if err != nil {
		panic(err)
	}
	return doc
}
