package services

// Multi-party variable bag tests (t-paliad-277 / m/paliad#109).
//
// Pins the three coexisting forms that addPartyVars emits per role:
//
//   - Comma-joined list:      parties.claimants / .defendants / .others
//   - Indexed access:         parties.claimant.0.name, parties.defendant.0.name, …
//   - Flat legacy (first-of): parties.claimant.name, parties.defendant.name, …
//
// Also covers filterPartiesBySelection — the empty-selection default
// (every party included) and the non-empty restriction.

import (
	"strings"
	"testing"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/models"
)

func mkParty(name, role, rep string) models.Party {
	p := models.Party{
		ID:   uuid.New(),
		Name: name,
	}
	if role != "" {
		r := role
		p.Role = &r
	}
	if rep != "" {
		r := rep
		p.Representative = &r
	}
	return p
}

func TestAddPartyVars_MultiPartyMixedRoles(t *testing.T) {
	t.Parallel()
	parties := []models.Party{
		mkParty("Acme Inc.", "claimant", "Maria Schmidt"),
		mkParty("Globex GmbH", "claimant", ""),
		mkParty("Initech", "defendant", "John Doe"),
		mkParty("Streithelferin", "intervenor", ""),
	}
	bag := PlaceholderMap{}
	addPartyVars(bag, parties)

	wants := map[string]string{
		// Comma-joined per role.
		"parties.claimants":                  "Acme Inc., Globex GmbH",
		"parties.claimants.representatives":  "Maria Schmidt", // Globex has no rep → skipped from join.
		"parties.defendants":                 "Initech",
		"parties.defendants.representatives": "John Doe",
		"parties.others":                     "Streithelferin",
		"parties.others.representatives":     "",
		// Indexed access.
		"parties.claimant.0.name":            "Acme Inc.",
		"parties.claimant.0.representative":  "Maria Schmidt",
		"parties.claimant.1.name":            "Globex GmbH",
		"parties.claimant.1.representative":  "",
		"parties.defendant.0.name":           "Initech",
		"parties.defendant.0.representative": "John Doe",
		"parties.other.0.name":               "Streithelferin",
		// Flat legacy: first-of-role.
		"parties.claimant.name":            "Acme Inc.",
		"parties.claimant.representative":  "Maria Schmidt",
		"parties.defendant.name":           "Initech",
		"parties.defendant.representative": "John Doe",
		"parties.other.name":               "Streithelferin",
	}
	for key, want := range wants {
		got, ok := bag[key]
		if !ok {
			t.Errorf("missing key %q in bag", key)
			continue
		}
		if got != want {
			t.Errorf("bag[%q] = %q, want %q", key, got, want)
		}
	}
}

func TestAddPartyVars_GermanRoleStrings(t *testing.T) {
	t.Parallel()
	// German role strings on real-world data must bucket the same as
	// the English equivalents — "Kläger" / "Klägerin" → claimants.
	parties := []models.Party{
		mkParty("Erika Musterfrau", "Klägerin", ""),
		mkParty("Max Mustermann", "Beklagter", ""),
	}
	bag := PlaceholderMap{}
	addPartyVars(bag, parties)

	if got := bag["parties.claimants"]; got != "Erika Musterfrau" {
		t.Errorf("parties.claimants = %q, want %q", got, "Erika Musterfrau")
	}
	if got := bag["parties.defendants"]; got != "Max Mustermann" {
		t.Errorf("parties.defendants = %q, want %q", got, "Max Mustermann")
	}
	// Backward-compat: legacy flat alias resolves to the first row of
	// the German-bucketed group.
	if got := bag["parties.claimant.name"]; got != "Erika Musterfrau" {
		t.Errorf("parties.claimant.name = %q, want %q", got, "Erika Musterfrau")
	}
}

func TestAddPartyVars_BackwardCompatFlatAliasResolvesFirstRow(t *testing.T) {
	t.Parallel()
	// Critical guarantee from m/paliad#109: templates that say
	// {{parties.claimant.name}} (old shape) must keep merging — they
	// resolve to the FIRST selected claimant. Pinning this stops a
	// future refactor silently dropping the alias and breaking every
	// .docx in the repo.
	parties := []models.Party{
		mkParty("FirstCo", "claimant", "Repr A"),
		mkParty("SecondCo", "claimant", "Repr B"),
	}
	bag := PlaceholderMap{}
	addPartyVars(bag, parties)
	if got := bag["parties.claimant.name"]; got != "FirstCo" {
		t.Errorf("parties.claimant.name (flat alias) = %q, want %q (first selected claimant)",
			got, "FirstCo")
	}
	if got := bag["parties.claimant.representative"]; got != "Repr A" {
		t.Errorf("parties.claimant.representative (flat alias) = %q, want %q",
			got, "Repr A")
	}
}

func TestFilterPartiesBySelection_EmptyMeansAll(t *testing.T) {
	t.Parallel()
	parties := []models.Party{
		mkParty("A", "claimant", ""),
		mkParty("B", "defendant", ""),
	}
	got := filterPartiesBySelection(parties, nil)
	if len(got) != 2 {
		t.Fatalf("empty selection should include every party, got %d/%d", len(got), len(parties))
	}
	got = filterPartiesBySelection(parties, []uuid.UUID{})
	if len(got) != 2 {
		t.Fatalf("empty []uuid selection should include every party, got %d/%d", len(got), len(parties))
	}
}

func TestFilterPartiesBySelection_NonEmptyRestricts(t *testing.T) {
	t.Parallel()
	a := mkParty("Acme", "claimant", "")
	b := mkParty("Initech", "defendant", "")
	c := mkParty("Globex", "claimant", "")
	parties := []models.Party{a, b, c}

	got := filterPartiesBySelection(parties, []uuid.UUID{a.ID, c.ID})
	if len(got) != 2 {
		t.Fatalf("got %d parties, want 2", len(got))
	}
	// Order must match the input order (PartyService.ListForProject
	// returns by name ascending; we preserve that to keep "first
	// claimant" deterministic across renders).
	if got[0].ID != a.ID || got[1].ID != c.ID {
		t.Errorf("selection lost input order: got %v", []string{got[0].Name, got[1].Name})
	}

	// The "Initech" defendant was deselected; the bag should not list
	// it under defendants.
	bag := PlaceholderMap{}
	addPartyVars(bag, got)
	if v, ok := bag["parties.defendants"]; ok && v != "" {
		t.Errorf("parties.defendants = %q after deselecting Initech, want empty", v)
	}
	if !strings.Contains(bag["parties.claimants"], "Acme") || !strings.Contains(bag["parties.claimants"], "Globex") {
		t.Errorf("parties.claimants = %q, want both Acme and Globex", bag["parties.claimants"])
	}
}

func TestIsProjectDerivedKey(t *testing.T) {
	t.Parallel()
	derived := []string{
		"project.title", "project.proceeding.name",
		"parties.claimants", "parties.claimant.0.name",
		"deadline.due_date",
		"procedural_event.name", "rule.name",
	}
	for _, k := range derived {
		if !isProjectDerivedKey(k) {
			t.Errorf("expected %q to be project-derived", k)
		}
	}
	survives := []string{
		"firm.name", "today", "today.long_de",
		"user.email", "user.display_name",
	}
	for _, k := range survives {
		if isProjectDerivedKey(k) {
			t.Errorf("expected %q to survive Import-from-project (firm/today/user namespace)", k)
		}
	}
}
