package docx

// Submission merge-engine tests — resurrected from the original
// t-paliad-215 Slice 1 (commit 8ea3509) + Slice 2 (commit 1765d5e).
// Adapted: helper names suffixed with "Merge" so they don't collide
// with the convert tests in submission_render_test.go (minimalDOTM,
// unzipEntries) that test the format-only ConvertDotmToDocx path.

import (
	"archive/zip"
	"bytes"
	"io"
	"strings"
	"testing"

	"mgit.msbls.de/m/paliad/pkg/docforge"
)

// minimalMergeDOCX builds a tiny .docx zip with one document.xml that
// contains the given body. Just enough to exercise the merge engine.
func minimalMergeDOCX(t *testing.T, documentBody string) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	w, err := zw.Create("word/document.xml")
	if err != nil {
		t.Fatalf("create document.xml: %v", err)
	}
	if _, err := io.WriteString(w, documentBody); err != nil {
		t.Fatalf("write document.xml: %v", err)
	}
	w2, err := zw.Create("[Content_Types].xml")
	if err != nil {
		t.Fatalf("create content types: %v", err)
	}
	// Use a docx-compatible content type so the convert pre-pass treats
	// the input as already-clean (no .dotm rewrites needed).
	body := `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
		`<Override PartName="/word/document.xml" ContentType="` + docxMainContentType + `"/></Types>`
	if _, err := io.WriteString(w2, body); err != nil {
		t.Fatalf("write content types: %v", err)
	}
	if err := zw.Close(); err != nil {
		t.Fatalf("close zip: %v", err)
	}
	return buf.Bytes()
}

// readMergeDocumentXML pulls word/document.xml out of a rendered .docx.
func readMergeDocumentXML(t *testing.T, b []byte) string {
	t.Helper()
	zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
	if err != nil {
		t.Fatalf("open rendered zip: %v", err)
	}
	for _, f := range zr.File {
		if f.Name != "word/document.xml" {
			continue
		}
		rc, err := f.Open()
		if err != nil {
			t.Fatalf("open document.xml: %v", err)
		}
		defer rc.Close()
		body, err := io.ReadAll(rc)
		if err != nil {
			t.Fatalf("read document.xml: %v", err)
		}
		return string(body)
	}
	t.Fatal("rendered .docx had no word/document.xml")
	return ""
}

func TestRender_SingleRunPlaceholder(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{firm.name}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{"firm.name": "HLC"}, nil)
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, ">HLC<") {
		t.Errorf("expected HLC in body, got %q", body)
	}
	if strings.Contains(body, "{{") {
		t.Errorf("unreplaced placeholder marker in body: %q", body)
	}
}

func TestRender_MultiplePlaceholdersPerRun(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{parties.claimant.name}}, vertreten durch {{parties.claimant.representative}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{
		"parties.claimant.name":           "Acme Inc.",
		"parties.claimant.representative": "Kanzlei Müller",
	}, nil)
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, "Acme Inc.") || !strings.Contains(body, "Kanzlei Müller") {
		t.Errorf("expected both party values, got %q", body)
	}
	if strings.Contains(body, "{{") {
		t.Errorf("unreplaced placeholder marker in body: %q", body)
	}
}

func TestRender_MissingMarker(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{project.case_number}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{}, docforge.DefaultMissingMarker("de"))
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, "[KEIN WERT: project.case_number]") {
		t.Errorf("expected KEIN WERT marker, got %q", body)
	}
	outEN, err := r.Render(tmpl, docforge.PlaceholderMap{}, docforge.DefaultMissingMarker("en"))
	if err != nil {
		t.Fatalf("render en: %v", err)
	}
	bodyEN := readMergeDocumentXML(t, outEN)
	if !strings.Contains(bodyEN, "[NO VALUE: project.case_number]") {
		t.Errorf("expected NO VALUE marker, got %q", bodyEN)
	}
}

func TestRender_CrossRunPlaceholder(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>Hello {{</w:t></w:r><w:r><w:t>project</w:t></w:r><w:r><w:t>.case_number}}!</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{"project.case_number": "7 O 1234/26"}, nil)
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, "7 O 1234/26") {
		t.Errorf("expected case number after cross-run merge, got %q", body)
	}
	if strings.Contains(body, "{{") {
		t.Errorf("orphan placeholder marker remained: %q", body)
	}
}

func TestRender_XMLEscaping(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{user.display_name}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{
		"user.display_name": `Müller & Söhne <GmbH> "Special"`,
	}, nil)
	if err != nil {
		t.Fatalf("render: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, "Müller &amp; Söhne &lt;GmbH&gt; &quot;Special&quot;") {
		t.Errorf("expected escaped value, got %q", body)
	}
}

func TestPlaceholderRegex_Boundaries(t *testing.T) {
	tests := []struct {
		in      string
		matches []string
	}{
		{"plain text", nil},
		{"{{foo}}", []string{"{{foo}}"}},
		{"{{ foo }}", []string{"{{ foo }}"}},
		{"{{foo.bar}}", []string{"{{foo.bar}}"}},
		{"{{ foo.bar_baz }}", []string{"{{ foo.bar_baz }}"}},
		{"{{1bad}}", nil},
		{"{{ foo }} and {{ bar }}", []string{"{{ foo }}", "{{ bar }}"}},
	}
	for _, tc := range tests {
		t.Run(tc.in, func(t *testing.T) {
			got := placeholderRegex.FindAllString(tc.in, -1)
			if len(got) != len(tc.matches) {
				t.Fatalf("got %d matches, want %d (in=%q)", len(got), len(tc.matches), tc.in)
			}
			for i := range got {
				if got[i] != tc.matches[i] {
					t.Errorf("match %d: got %q, want %q", i, got[i], tc.matches[i])
				}
			}
		})
	}
}

// TestRenderHTML_ExtractsParagraphsAndFormatting verifies the preview
// HTML emitter walks <w:p> / <w:r> / <w:t> correctly and carries
// bold/italic through to <strong>/<em>. Substituted placeholders are
// wrapped in <span class="draft-var" data-var="…"> so the client can
// make them clickable (t-paliad-261).
func TestRenderHTML_ExtractsParagraphsAndFormatting(t *testing.T) {
	doc := `<w:document><w:body>` +
		`<w:p><w:r><w:t>Hello {{firm.name}}</w:t></w:r></w:p>` +
		`<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>Bold line</w:t></w:r></w:p>` +
		`<w:p><w:r><w:rPr><w:i/></w:rPr><w:t>Italic line</w:t></w:r></w:p>` +
		`</w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	html, err := r.RenderHTML(tmpl, docforge.PlaceholderMap{"firm.name": "HLC"}, nil)
	if err != nil {
		t.Fatalf("render html: %v", err)
	}
	if !strings.Contains(html, `<p>Hello <span class="draft-var" data-var="firm.name">HLC</span></p>`) {
		t.Errorf("expected merged paragraph with draft-var span, got %q", html)
	}
	if !strings.Contains(html, "<strong>Bold line</strong>") {
		t.Errorf("expected bold span, got %q", html)
	}
	if !strings.Contains(html, "<em>Italic line</em>") {
		t.Errorf("expected italic span, got %q", html)
	}
}

// TestRenderHTML_EscapesContent confirms the preview emitter HTML-escapes
// special characters in placeholder values even inside the draft-var
// span wrapper.
func TestRenderHTML_EscapesContent(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{user.display_name}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	html, err := r.RenderHTML(tmpl, docforge.PlaceholderMap{
		"user.display_name": `M&S <Inc> "X"`,
	}, nil)
	if err != nil {
		t.Fatalf("render html: %v", err)
	}
	want := `<span class="draft-var" data-var="user.display_name">M&amp;S &lt;Inc&gt; &quot;X&quot;</span>`
	if !strings.Contains(html, want) {
		t.Errorf("expected escaped value inside draft-var span, got %q", html)
	}
}

// TestRenderHTML_WrapsMissingMarker confirms that an unbound placeholder
// is still rendered as a clickable draft-var span so the user can click
// the [KEIN WERT: …] marker in the preview and jump to the field.
func TestRenderHTML_WrapsMissingMarker(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{project.case_number}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	html, err := r.RenderHTML(tmpl, docforge.PlaceholderMap{}, nil)
	if err != nil {
		t.Fatalf("render html: %v", err)
	}
	want := `<span class="draft-var" data-var="project.case_number">[KEIN WERT: project.case_number]</span>`
	if !strings.Contains(html, want) {
		t.Errorf("expected missing marker wrapped in draft-var span, got %q", html)
	}
}

// TestRenderHTML_WrapsOverriddenValueSameAsResolved is the t-paliad-274
// regression: m's report on m/paliad#106 was that "When filled, the link
// disappears". The preview HTML must wrap an override value with the
// same <span class="draft-var"> as it would an unfilled placeholder, so
// the click-jump from preview→sidebar persists after the user types a
// value. There is no distinction at the renderer level between a value
// that came from the resolved bag (project / parties / deadline lookups)
// and a value the lawyer typed into the sidebar — both arrive in the
// same docforge.PlaceholderMap and both must be wrapped.
func TestRenderHTML_WrapsOverriddenValueSameAsResolved(t *testing.T) {
	doc := `<w:document><w:body>` +
		`<w:p><w:r><w:t>{{project.case_number}} / {{firm.name}}</w:t></w:r></w:p>` +
		`</w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	// project.case_number is the typed-by-lawyer override.
	// firm.name is the always-resolved value from the firm bag.
	html, err := r.RenderHTML(tmpl, docforge.PlaceholderMap{
		"project.case_number": "UPC_CFI_42/2026",
		"firm.name":           "HLC",
	}, nil)
	if err != nil {
		t.Fatalf("render html: %v", err)
	}
	wantOverride := `<span class="draft-var" data-var="project.case_number">UPC_CFI_42/2026</span>`
	if !strings.Contains(html, wantOverride) {
		t.Errorf("expected overridden value wrapped in draft-var span (click-jump must persist after fill, t-paliad-274), got %q", html)
	}
	wantResolved := `<span class="draft-var" data-var="firm.name">HLC</span>`
	if !strings.Contains(html, wantResolved) {
		t.Errorf("expected resolved value still wrapped, got %q", html)
	}
}

// TestRender_DocxOutputUnchangedByPreviewWrap asserts the hard rule from
// t-paliad-261: the .docx export path must NOT carry the preview-only
// draft-var sentinels or any draft-var span markup. Renders the same
// template through Render (.docx) and asserts the merged document.xml
// has only the resolved value, not a wrapped one.
func TestRender_DocxOutputUnchangedByPreviewWrap(t *testing.T) {
	doc := `<w:document><w:body><w:p><w:r><w:t>{{firm.name}}</w:t></w:r></w:p></w:body></w:document>`
	tmpl := minimalMergeDOCX(t, doc)
	r := NewSubmissionRenderer()
	out, err := r.Render(tmpl, docforge.PlaceholderMap{"firm.name": "HLC"}, nil)
	if err != nil {
		t.Fatalf("render docx: %v", err)
	}
	body := readMergeDocumentXML(t, out)
	if !strings.Contains(body, `<w:t>HLC</w:t>`) {
		t.Errorf("expected raw resolved value in .docx, got %q", body)
	}
	// PUA sentinels and any span markup must NOT appear in the .docx.
	for _, forbidden := range []string{"draft-var", "data-var", previewVarBegin, previewVarMid, previewVarEnd} {
		if strings.Contains(body, forbidden) {
			t.Errorf("docx output unexpectedly contains %q: %q", forbidden, body)
		}
	}
}
