package services

// Live-DB test for generation-on-uploaded-templates (t-paliad-349 slice 7).
// Skipped without TEST_DATABASE_URL. Verifies the shipped draft-service
// change end-to-end against real Postgres:
//   1. submission_drafts.template_version_id round-trips through
//      Update → Get (the column-sync + patch path), and clears to NULL.
//   2. An uploaded template's carrier renders via the v1 Export path:
//      {{firm.name}} in the carrier substitutes to the branding name.
//
// This is the verification the head greenlit (option C) before the
// shipped-code change is committed.

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

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

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

func TestSubmissionDraft_TemplateVersionPin(t *testing.T) {
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
	}
	if err := db.ApplyMigrations(url); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}
	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer pool.Close()
	ctx := context.Background()

	userID := uuid.New()
	email := "tplpin-" + userID.String()[:8] + "@hlc.com"
	cleanup := func() {
		pool.ExecContext(ctx, `DELETE FROM paliad.submission_drafts WHERE user_id = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM paliad.templates WHERE created_by = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id = $1`, userID)
		pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id = $1`, userID)
	}
	cleanup()
	defer cleanup()
	if _, err := pool.ExecContext(ctx, `INSERT INTO auth.users (id, email) VALUES ($1, $2)`, userID, email); err != nil {
		t.Fatalf("seed auth.users: %v", err)
	}
	if _, err := pool.ExecContext(ctx,
		`INSERT INTO paliad.users (id, email, display_name, office, global_role, lang)
		 VALUES ($1, $2, 'Tpl Pin', 'munich', 'standard', 'de')`, userID, email); err != nil {
		t.Fatalf("seed paliad.users: %v", err)
	}

	users := NewUserService(pool)
	projects := NewProjectService(pool, users)
	parties := NewPartyService(pool, projects)
	vars := NewSubmissionVarsService(pool, projects, parties, users)
	renderer := NewSubmissionRenderer()
	drafts := NewSubmissionDraftService(pool, projects, vars, renderer)
	store := NewPgTemplateStore(pool)

	// Uploaded template whose carrier carries a {{firm.name}} slot.
	carrier := minimalDocxWithBody(t, `<w:p><w:r><w:t>Von {{firm.name}}</w:t></w:r></w:p>`)
	tmpl, err := store.Create(ctx,
		docforge.TemplateMetaInput{NameDE: "Pin-Test", NameEN: "Pin test", CreatedBy: userID.String()},
		docforge.TemplateVersionInput{CarrierBytes: carrier, CreatedBy: userID.String()})
	if err != nil {
		t.Fatalf("store.Create: %v", err)
	}
	if tmpl.VersionID == "" {
		t.Fatalf("template VersionID empty — generation can't pin it")
	}
	versionID := uuid.MustParse(tmpl.VersionID)

	// Project-less draft on a code that has a published rule (so Build
	// resolves). No composer attached → plain draft.
	d, err := drafts.Create(ctx, userID, nil, "de.inf.lg.erwidg", "de")
	if err != nil {
		t.Fatalf("drafts.Create: %v", err)
	}
	if d.TemplateVersionID != nil {
		t.Errorf("fresh draft has a template pin: %v", d.TemplateVersionID)
	}

	// --- Pin the version via Update, read it back via Get.
	pin := &versionID
	if _, err := drafts.Update(ctx, userID, d.ID, DraftPatch{TemplateVersionID: &pin}); err != nil {
		t.Fatalf("Update(pin): %v", err)
	}
	got, err := drafts.Get(ctx, userID, d.ID)
	if err != nil {
		t.Fatalf("Get after pin: %v", err)
	}
	if got.TemplateVersionID == nil || *got.TemplateVersionID != versionID {
		t.Fatalf("pinned template_version_id = %v; want %s", got.TemplateVersionID, versionID)
	}

	// --- The uploaded carrier renders via Export: {{firm.name}} → "HLC".
	out, _, err := drafts.Export(ctx, got, carrier)
	if err != nil {
		t.Fatalf("Export: %v", err)
	}
	doc := unzipDocumentXML(t, out)
	if strings.Contains(doc, "{{firm.name}}") {
		t.Errorf("placeholder not substituted; doc=%s", doc)
	}
	if !strings.Contains(doc, "HLC") {
		t.Errorf("firm.name did not resolve to HLC; doc=%s", doc)
	}

	// --- Clearing the pin sets it back to NULL.
	var nilPin *uuid.UUID
	if _, err := drafts.Update(ctx, userID, d.ID, DraftPatch{TemplateVersionID: &nilPin}); err != nil {
		t.Fatalf("Update(clear): %v", err)
	}
	cleared, err := drafts.Get(ctx, userID, d.ID)
	if err != nil {
		t.Fatalf("Get after clear: %v", err)
	}
	if cleared.TemplateVersionID != nil {
		t.Errorf("template_version_id = %v after clear; want nil", cleared.TemplateVersionID)
	}
}

// minimalDocxWithBody builds a tiny valid .docx (zip) whose document.xml
// body is the given inner XML.
func minimalDocxWithBody(t *testing.T, inner string) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	add := func(name, body string) {
		w, err := zw.Create(name)
		if err != nil {
			t.Fatalf("zip create %s: %v", name, err)
		}
		if _, err := io.WriteString(w, body); err != nil {
			t.Fatalf("zip write %s: %v", name, err)
		}
	}
	add("[Content_Types].xml",
		`<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`+
			`<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+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>`+inner+`</w:body></w:document>`)
	if err := zw.Close(); err != nil {
		t.Fatalf("zip close: %v", err)
	}
	return buf.Bytes()
}

func unzipDocumentXML(t *testing.T, b []byte) string {
	t.Helper()
	zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
	if err != nil {
		t.Fatalf("open 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()
		data, _ := io.ReadAll(rc)
		return string(data)
	}
	t.Fatal("document.xml not found in output")
	return ""
}
