package docx

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

// minimalDOTM builds a small .dotm zip whose shape mirrors the real
// HL Patents Style template: macro-enabled main content type, Default
// extension declaring .bin as vbaProject, Overrides for vbaData.xml +
// customizations.xml, document.xml.rels with vbaProject +
// keyMapCustomizations relationships, and the four macro parts on
// disk (vbaProject.bin + auxiliary rels + vbaData.xml +
// customizations.xml).
//
// In-memory so the test is self-contained (no checked-in binary).
// Word and LibreOffice would reject this minimal file as incomplete
// (no _rels/.rels root manifest); the tests work at the byte level
// and assert structural properties of the converted output.
func minimalDOTM(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	add := func(name, body string) {
		t.Helper()
		w, err := zw.CreateHeader(&zip.FileHeader{
			Name:     name,
			Method:   zip.Deflate,
			Modified: time.Date(2026, 5, 21, 12, 0, 0, 0, time.UTC),
		})
		if err != nil {
			t.Fatalf("zip header %s: %v", name, err)
		}
		if _, err := io.WriteString(w, body); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}

	add(contentTypesPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
		`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`+
		`<Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/>`+
		`<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>`+
		`<Default Extension="xml" ContentType="application/xml"/>`+
		`<Override PartName="/word/document.xml" ContentType="`+dotmMainContentType+`"/>`+
		`<Override PartName="/word/customizations.xml" ContentType="application/vnd.ms-word.keyMapCustomizations+xml"/>`+
		`<Override PartName="/word/vbaData.xml" ContentType="application/vnd.ms-word.vbaData+xml"/>`+
		`<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>`+
		`</Types>`)

	add("word/document.xml",
		`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
			`<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">`+
			`<w:body><w:p><w:r><w:t>Hello Paliad</w:t></w:r></w:p></w:body></w:document>`)

	add(documentRelsPath,
		`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
			`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`+
			`<Relationship Id="rId1" Type="http://schemas.microsoft.com/office/2006/relationships/vbaProject" Target="vbaProject.bin"/>`+
			`<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`+
			`<Relationship Id="rId3" Type="http://schemas.microsoft.com/office/2006/relationships/keyMapCustomizations" Target="customizations.xml"/>`+
			`</Relationships>`)

	add("word/styles.xml", `<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>`)
	add("word/vbaProject.bin", "PRETEND-VBA-BINARY-PAYLOAD")
	add("word/_rels/vbaProject.bin.rels", `<?xml version="1.0"?><Relationships/>`)
	add("word/vbaData.xml", `<?xml version="1.0"?><wne:vbaSuppData xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"/>`)
	add("word/customizations.xml", `<?xml version="1.0"?><wne:tcg xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"/>`)

	if err := zw.Close(); err != nil {
		t.Fatalf("close zip: %v", err)
	}
	return buf.Bytes()
}

func unzipEntries(t *testing.T, data []byte) map[string]string {
	t.Helper()
	zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
	if err != nil {
		t.Fatalf("open output zip: %v", err)
	}
	out := make(map[string]string, len(zr.File))
	for _, f := range zr.File {
		rc, err := f.Open()
		if err != nil {
			t.Fatalf("open %s: %v", f.Name, err)
		}
		body, err := io.ReadAll(rc)
		rc.Close()
		if err != nil {
			t.Fatalf("read %s: %v", f.Name, err)
		}
		out[f.Name] = string(body)
	}
	return out
}

func TestConvertDotmToDocx_StripsMacroParts(t *testing.T) {
	dotm := minimalDOTM(t)
	out, err := ConvertDotmToDocx(dotm)
	if err != nil {
		t.Fatalf("ConvertDotmToDocx: %v", err)
	}

	entries := unzipEntries(t, out)

	for _, name := range []string{
		"word/vbaProject.bin",
		"word/_rels/vbaProject.bin.rels",
		"word/vbaData.xml",
		"word/customizations.xml",
	} {
		if _, ok := entries[name]; ok {
			t.Errorf("output still contains %s", name)
		}
	}
	if doc, ok := entries["word/document.xml"]; !ok {
		t.Error("output is missing word/document.xml")
	} else if !strings.Contains(doc, "Hello Paliad") {
		t.Errorf("document body lost during conversion: %q", doc)
	}
	if _, ok := entries["word/styles.xml"]; !ok {
		t.Error("output lost unrelated word/styles.xml")
	}

	ctypes, ok := entries[contentTypesPath]
	if !ok {
		t.Fatal("output is missing [Content_Types].xml")
	}
	if strings.Contains(ctypes, "macroEnabled") {
		t.Errorf("output [Content_Types].xml still references a macro-enabled type: %q", ctypes)
	}
	if !strings.Contains(ctypes, docxMainContentType) {
		t.Errorf("output is missing plain docx main content type: %q", ctypes)
	}
	if strings.Contains(ctypes, "vbaProject") {
		t.Errorf("output [Content_Types].xml still references vbaProject: %q", ctypes)
	}
	if strings.Contains(ctypes, "vbaData") {
		t.Errorf("output [Content_Types].xml still overrides vbaData: %q", ctypes)
	}
	if strings.Contains(ctypes, "keyMapCustomizations") {
		t.Errorf("output [Content_Types].xml still overrides customizations: %q", ctypes)
	}
	if !strings.Contains(ctypes, "wordprocessingml.styles") {
		t.Errorf("output lost unrelated styles Override: %q", ctypes)
	}

	rels, ok := entries[documentRelsPath]
	if !ok {
		t.Fatal("output is missing word/_rels/document.xml.rels")
	}
	if strings.Contains(rels, "vbaProject") {
		t.Errorf("output rels still references vbaProject: %q", rels)
	}
	if strings.Contains(rels, "keyMapCustomizations") {
		t.Errorf("output rels still references keyMapCustomizations: %q", rels)
	}
	if !strings.Contains(rels, "styles.xml") {
		t.Errorf("output rels lost unrelated styles relationship: %q", rels)
	}
}

func TestConvertDotmToDocx_IdempotentOnPlainDocx(t *testing.T) {
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	add := func(name, body string) {
		w, err := zw.Create(name)
		if err != nil {
			t.Fatalf("create %s: %v", name, err)
		}
		if _, err := io.WriteString(w, body); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	add(contentTypesPath, `<?xml version="1.0"?>`+
		`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`+
		`<Override PartName="/word/document.xml" ContentType="`+docxMainContentType+`"/>`+
		`</Types>`)
	add("word/document.xml", `<w:document/>`)
	if err := zw.Close(); err != nil {
		t.Fatalf("close: %v", err)
	}

	out, err := ConvertDotmToDocx(buf.Bytes())
	if err != nil {
		t.Fatalf("ConvertDotmToDocx: %v", err)
	}

	entries := unzipEntries(t, out)
	if _, ok := entries["word/vbaProject.bin"]; ok {
		t.Error("plain docx grew a vbaProject during conversion")
	}
	if ctypes := entries[contentTypesPath]; !strings.Contains(ctypes, docxMainContentType) {
		t.Errorf("plain docx lost its content type: %q", ctypes)
	}
}

func TestConvertDotmToDocx_AcceptsDocmAndDotx(t *testing.T) {
	for _, mainType := range []string{docmMainContentType, dotxMainContentType} {
		t.Run(mainType, func(t *testing.T) {
			var buf bytes.Buffer
			zw := zip.NewWriter(&buf)
			add := func(name, body string) {
				w, _ := zw.Create(name)
				_, _ = io.WriteString(w, body)
			}
			add(contentTypesPath, `<?xml version="1.0"?>`+
				`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`+
				`<Override PartName="/word/document.xml" ContentType="`+mainType+`"/>`+
				`</Types>`)
			add("word/document.xml", `<w:document/>`)
			zw.Close()
			out, err := ConvertDotmToDocx(buf.Bytes())
			if err != nil {
				t.Fatalf("ConvertDotmToDocx: %v", err)
			}
			ctypes := unzipEntries(t, out)[contentTypesPath]
			if strings.Contains(ctypes, mainType) {
				t.Errorf("non-docx main type survived conversion: %q", ctypes)
			}
			if !strings.Contains(ctypes, docxMainContentType) {
				t.Errorf("docx main type not present: %q", ctypes)
			}
		})
	}
}

func TestConvertDotmToDocx_RejectsNonZip(t *testing.T) {
	_, err := ConvertDotmToDocx([]byte("not a zip file"))
	if err == nil {
		t.Fatal("expected error for non-zip input, got nil")
	}
}

func TestSanitiseSubmissionFileName(t *testing.T) {
	cases := map[string]string{
		"Klageerwiderung":        "Klageerwiderung",
		"Berufungsbegründung":    "Berufungsbegruendung",
		"Schriftsatz/Anlage":     "Schriftsatz_Anlage",
		`Statement of "Defence"`:  "Statement of Defence",
		`  Klage  `:               "Klage",
		"Größe":                   "Groesse",
		"UPC_CFI_123/2026":        "UPC_CFI_123_2026",
		"a:b*c?d<e>f|g":           "a_b_c_d_e_f_g",
		"Klageerwiderung (Frist)": "Klageerwiderung (Frist)",
	}
	for in, want := range cases {
		t.Run(in, func(t *testing.T) {
			if got := SanitiseSubmissionFileName(in); got != want {
				t.Errorf("SanitiseSubmissionFileName(%q) = %q, want %q", in, got, want)
			}
		})
	}
}
