package docs

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"strings"
	"testing"
)

// richDocJSON is a Tiptap document exercising headings, marks, and a list —
// including CJK text, which every format must carry through intact.
const richDocJSON = `{"type":"doc","content":[` +
	`{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"My Section"}]},` +
	`{"type":"paragraph","content":[{"type":"text","text":"Hello "},{"type":"text","marks":[{"type":"bold"}],"text":"world"},{"type":"text","text":" 你好"}]},` +
	`{"type":"bulletList","content":[` +
	`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"first"}]}]},` +
	`{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"second"}]}]}` +
	`]}` +
	`]}`

func seedRichDoc(t *testing.T, srv http.Handler) string {
	t.Helper()
	id := newDoc(t, srv)
	body := `{"title":"日记 Diary","content":` + jsonString(richDocJSON) + `,"content_text":"My Section\nHello world 你好\nfirst\nsecond","word_count":6}`
	if rec := do(t, srv, http.MethodPut, "/"+id, body); rec.Code != http.StatusOK {
		t.Fatalf("seed rich doc: code=%d body=%s", rec.Code, rec.Body)
	}
	return id
}

// jsonString quotes a string as a JSON string literal (so the Tiptap JSON can be
// embedded as the "content" field value).
func jsonString(s string) string {
	b, _ := json.Marshal(s)
	return string(b)
}

func TestExportMarkdown(t *testing.T) {
	srv := newTestServer(t)
	id := seedRichDoc(t, srv)

	rec := do(t, srv, http.MethodGet, "/"+id+"/export?format=md", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("export md: code=%d body=%s", rec.Code, rec.Body)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/markdown") {
		t.Fatalf("unexpected content-type: %q", ct)
	}
	out := rec.Body.String()
	for _, want := range []string{"## My Section", "**world**", "你好", "- first", "- second"} {
		if !strings.Contains(out, want) {
			t.Fatalf("markdown missing %q in:\n%s", want, out)
		}
	}
	// CJK filename must survive in the RFC 5987 form.
	if cd := rec.Header().Get("Content-Disposition"); !strings.Contains(cd, "filename*=UTF-8''") {
		t.Fatalf("expected RFC 5987 filename, got %q", cd)
	}
}

func TestExportAll(t *testing.T) {
	srv := newTestServer(t)
	// Two docs, one with a duplicate title to exercise filename de-duplication.
	seedRichDoc(t, srv)
	id2 := newDoc(t, srv)
	if rec := do(t, srv, http.MethodPut, "/"+id2, `{"title":"日记 Diary","content":`+jsonString(richDocJSON)+`,"content_text":"x","word_count":1}`); rec.Code != http.StatusOK {
		t.Fatalf("seed second doc: %d %s", rec.Code, rec.Body)
	}

	rec := do(t, srv, http.MethodGet, "/export-all?format=md", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("export-all: code=%d body=%s", rec.Code, rec.Body)
	}
	if ct := rec.Header().Get("Content-Type"); ct != "application/zip" {
		t.Fatalf("unexpected content-type: %q", ct)
	}
	zr, err := zip.NewReader(bytes.NewReader(rec.Body.Bytes()), int64(rec.Body.Len()))
	if err != nil {
		t.Fatalf("zip open: %v", err)
	}
	if len(zr.File) != 2 {
		t.Fatalf("expected 2 files in backup, got %d", len(zr.File))
	}
	names := map[string]bool{}
	for _, f := range zr.File {
		names[f.Name] = true
		rc, _ := f.Open()
		b, _ := io.ReadAll(rc)
		rc.Close()
		if !strings.Contains(string(b), "你好") {
			t.Fatalf("backup entry %q missing CJK body", f.Name)
		}
	}
	// The duplicate title must have been disambiguated, not overwritten.
	if !names["日记 Diary.md"] || !names["日记 Diary (1).md"] {
		t.Fatalf("expected de-duplicated filenames, got %v", names)
	}
}

func TestExportHTML(t *testing.T) {
	srv := newTestServer(t)
	id := seedRichDoc(t, srv)

	rec := do(t, srv, http.MethodGet, "/"+id+"/export?format=html", "")
	out := rec.Body.String()
	for _, want := range []string{"<!doctype html>", "<h2>My Section</h2>", "<strong>world</strong>", "你好", "<li>first</li>"} {
		if !strings.Contains(out, want) {
			t.Fatalf("html missing %q", want)
		}
	}
}

func TestExportPlainText(t *testing.T) {
	srv := newTestServer(t)
	id := seedRichDoc(t, srv)

	rec := do(t, srv, http.MethodGet, "/"+id+"/export?format=txt", "")
	out := rec.Body.String()
	for _, want := range []string{"My Section", "Hello world 你好", "• first"} {
		if !strings.Contains(out, want) {
			t.Fatalf("txt missing %q in:\n%s", want, out)
		}
	}
}

func TestExportDocx(t *testing.T) {
	srv := newTestServer(t)
	id := seedRichDoc(t, srv)

	rec := do(t, srv, http.MethodGet, "/"+id+"/export?format=docx", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("export docx: code=%d", rec.Code)
	}
	raw := rec.Body.Bytes()
	zr, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
	if err != nil {
		t.Fatalf("docx is not a valid zip: %v", err)
	}

	want := map[string]bool{"[Content_Types].xml": false, "word/document.xml": false}
	var docXML string
	for _, f := range zr.File {
		if _, ok := want[f.Name]; ok {
			want[f.Name] = true
		}
		if f.Name == "word/document.xml" {
			rc, _ := f.Open()
			b, _ := io.ReadAll(rc)
			rc.Close()
			docXML = string(b)
		}
	}
	for name, found := range want {
		if !found {
			t.Fatalf("docx missing part %q", name)
		}
	}
	for _, w := range []string{"My Section", "你好", "<w:b/>", "Heading2"} {
		if !strings.Contains(docXML, w) {
			t.Fatalf("document.xml missing %q", w)
		}
	}
}

// richDocJSON2 exercises the newer node/mark types: links, highlight, an image,
// and a table (with a header row). Each export format should carry them through.
const richDocJSON2 = `{"type":"doc","content":[` +
	`{"type":"paragraph","content":[` +
	`{"type":"text","marks":[{"type":"link","attrs":{"href":"https://petal.test"}}],"text":"site"},` +
	`{"type":"text","text":" and "},` +
	`{"type":"text","marks":[{"type":"highlight","attrs":{"color":"#FFF1A8"}}],"text":"lit"}` +
	`]},` +
	`{"type":"image","attrs":{"src":"/api/images/abc123.png","alt":"a cat"}},` +
	`{"type":"table","content":[` +
	`{"type":"tableRow","content":[` +
	`{"type":"tableHeader","content":[{"type":"paragraph","content":[{"type":"text","text":"Name"}]}]},` +
	`{"type":"tableHeader","content":[{"type":"paragraph","content":[{"type":"text","text":"年龄"}]}]}` +
	`]},` +
	`{"type":"tableRow","content":[` +
	`{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"Mei"}]}]},` +
	`{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"30"}]}]}` +
	`]}` +
	`]}` +
	`]}`

func seedRichDoc2(t *testing.T, srv http.Handler) string {
	t.Helper()
	id := newDoc(t, srv)
	body := `{"title":"Rich2","content":` + jsonString(richDocJSON2) + `,"content_text":"site and lit\nName 年龄 Mei 30","word_count":6}`
	if rec := do(t, srv, http.MethodPut, "/"+id, body); rec.Code != http.StatusOK {
		t.Fatalf("seed rich doc 2: code=%d body=%s", rec.Code, rec.Body)
	}
	return id
}

func TestExportLinksImagesTables(t *testing.T) {
	srv := newTestServer(t)
	id := seedRichDoc2(t, srv)

	md := do(t, srv, http.MethodGet, "/"+id+"/export?format=md", "").Body.String()
	for _, want := range []string{"[site](https://petal.test)", "![a cat](/api/images/abc123.png)", "| Name | 年龄 |", "| --- | --- |", "| Mei | 30 |"} {
		if !strings.Contains(md, want) {
			t.Fatalf("markdown missing %q in:\n%s", want, md)
		}
	}

	html := do(t, srv, http.MethodGet, "/"+id+"/export?format=html", "").Body.String()
	for _, want := range []string{`<a href="https://petal.test">site</a>`, "<mark>lit</mark>", `<img src="/api/images/abc123.png" alt="a cat">`, "<th>Name</th>", "<td>Mei</td>"} {
		if !strings.Contains(html, want) {
			t.Fatalf("html missing %q in:\n%s", want, html)
		}
	}

	docx := do(t, srv, http.MethodGet, "/"+id+"/export?format=docx", "")
	raw := docx.Body.Bytes()
	zr, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
	if err != nil {
		t.Fatalf("docx not a zip: %v", err)
	}
	var docXML string
	for _, f := range zr.File {
		if f.Name == "word/document.xml" {
			rc, _ := f.Open()
			b, _ := io.ReadAll(rc)
			rc.Close()
			docXML = string(b)
		}
	}
	for _, want := range []string{"<w:tbl>", "Name", "年龄", "[a cat]", "<w:highlight"} {
		if !strings.Contains(docXML, want) {
			t.Fatalf("docx document.xml missing %q", want)
		}
	}
}

func TestExportUnsupportedFormat(t *testing.T) {
	srv := newTestServer(t)
	id := newDoc(t, srv)
	if rec := do(t, srv, http.MethodGet, "/"+id+"/export?format=pdf", ""); rec.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for unsupported format, got %d", rec.Code)
	}
}
