package services

// Pure-function tests for the trigger-group duration sort introduced
// by t-paliad-296 / m/paliad#128. No DB needed — feeds synthetic
// UIDeadlines and a ruleByID map directly into the helper.

import (
	"testing"

	"github.com/google/uuid"

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

// makeRule is a tiny constructor for a synthetic rule with just the
// fields the sort reads (parent_id, duration_value, duration_unit,
// submission_code, trigger_event_id).
func makeRule(t *testing.T, parent *uuid.UUID, code string, val int, unit string) (uuid.UUID, models.DeadlineRule) {
	t.Helper()
	id := uuid.New()
	codeCopy := code
	return id, models.DeadlineRule{
		ID:             id,
		ParentID:       parent,
		SubmissionCode: &codeCopy,
		DurationValue:  val,
		DurationUnit:   unit,
	}
}

func makeDeadline(id uuid.UUID, code string) UIDeadline {
	return UIDeadline{
		RuleID: id.String(),
		Code:   code,
	}
}

// TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision is the
// canonical scenario from m's report — four post-decision optional
// events anchored on the same decision must render with 1-month rules
// before 2-month rules.
func TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision(t *testing.T) {
	decisionID := uuid.New()

	// Catalog order matches mig 132 sequence_order: cons_orders(60),
	// cost_app(70), rectification(70), appeal_spawn(80).
	consOrdID, consOrdRule := makeRule(t, &decisionID, "upc.inf.cfi.cons_orders", 2, "months")
	costAppID, costAppRule := makeRule(t, &decisionID, "upc.inf.cfi.cost_app", 1, "months")
	rectID, rectRule := makeRule(t, &decisionID, "upc.inf.cfi.rectification", 1, "months")
	appealID, appealRule := makeRule(t, &decisionID, "upc.inf.cfi.appeal_spawn", 2, "months")

	ruleByID := map[uuid.UUID]models.DeadlineRule{
		consOrdID: consOrdRule,
		costAppID: costAppRule,
		rectID:    rectRule,
		appealID:  appealRule,
	}

	deadlines := []UIDeadline{
		makeDeadline(consOrdID, "upc.inf.cfi.cons_orders"),
		makeDeadline(costAppID, "upc.inf.cfi.cost_app"),
		makeDeadline(rectID, "upc.inf.cfi.rectification"),
		makeDeadline(appealID, "upc.inf.cfi.appeal_spawn"),
	}

	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)

	// 1-month tier first (cost_app, rectification — alphabetical by
	// submission_code), then 2-month tier (appeal_spawn, cons_orders
	// — submission_code ASC tiebreak per spec).
	want := []string{
		"upc.inf.cfi.cost_app",
		"upc.inf.cfi.rectification",
		"upc.inf.cfi.appeal_spawn",
		"upc.inf.cfi.cons_orders",
	}
	for i, w := range want {
		if deadlines[i].Code != w {
			t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
		}
	}
}

// TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight asserts the
// unit-weight ordering: days < weeks < months < years, with shorter
// durations of the same unit winning their tier.
func TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight(t *testing.T) {
	parentID := uuid.New()

	d14ID, d14Rule := makeRule(t, &parentID, "x.14days", 14, "days")
	d2wID, d2wRule := makeRule(t, &parentID, "x.2weeks", 2, "weeks")
	d1mID, d1mRule := makeRule(t, &parentID, "x.1month", 1, "months")
	d6mID, d6mRule := makeRule(t, &parentID, "x.6months", 6, "months")
	d1yID, d1yRule := makeRule(t, &parentID, "x.1year", 1, "years")

	ruleByID := map[uuid.UUID]models.DeadlineRule{
		d14ID: d14Rule, d2wID: d2wRule, d1mID: d1mRule, d6mID: d6mRule, d1yID: d1yRule,
	}

	deadlines := []UIDeadline{
		makeDeadline(d6mID, "x.6months"),
		makeDeadline(d1yID, "x.1year"),
		makeDeadline(d2wID, "x.2weeks"),
		makeDeadline(d14ID, "x.14days"),
		makeDeadline(d1mID, "x.1month"),
	}

	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)

	want := []string{"x.14days", "x.2weeks", "x.1month", "x.6months", "x.1year"}
	for i, w := range want {
		if deadlines[i].Code != w {
			t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
		}
	}
}

// TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder
// guards the hard rule: rules with different parents must keep their
// relative position. Sorting only ever permutes adjacent same-parent
// rows.
func TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder(t *testing.T) {
	parentAID := uuid.New()
	parentBID := uuid.New()

	a3mID, a3mRule := makeRule(t, &parentAID, "ga.3months", 3, "months")
	b1mID, b1mRule := makeRule(t, &parentBID, "gb.1month", 1, "months")
	a14dID, a14dRule := makeRule(t, &parentAID, "ga.14days", 14, "days")
	b2mID, b2mRule := makeRule(t, &parentBID, "gb.2months", 2, "months")

	ruleByID := map[uuid.UUID]models.DeadlineRule{
		a3mID: a3mRule, b1mID: b1mRule, a14dID: a14dRule, b2mID: b2mRule,
	}

	// Interleaved groups: A, B, A, B. Each group has one rule between
	// each other group's rules — the consecutive-run walk should treat
	// each as its own one-element run and not reorder anything.
	deadlines := []UIDeadline{
		makeDeadline(a3mID, "ga.3months"),
		makeDeadline(b1mID, "gb.1month"),
		makeDeadline(a14dID, "ga.14days"),
		makeDeadline(b2mID, "gb.2months"),
	}

	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)

	want := []string{"ga.3months", "gb.1month", "ga.14days", "gb.2months"}
	for i, w := range want {
		if deadlines[i].Code != w {
			t.Errorf("deadlines[%d].Code = %q, want %q (interleaved groups must not reorder across)", i, deadlines[i].Code, w)
		}
	}
}

// TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast asserts
// that court-set / conditional rows (no concrete date in the duration
// ladder) sort LAST within their group, regardless of their stated
// duration value.
func TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast(t *testing.T) {
	parentID := uuid.New()

	dID, dRule := makeRule(t, &parentID, "x.duration", 2, "months")
	cID, cRule := makeRule(t, &parentID, "x.conditional", 1, "months")
	csID, csRule := makeRule(t, &parentID, "x.courtset", 1, "months")
	d2ID, d2Rule := makeRule(t, &parentID, "x.short", 14, "days")

	ruleByID := map[uuid.UUID]models.DeadlineRule{
		dID: dRule, cID: cRule, csID: csRule, d2ID: d2Rule,
	}

	deadlines := []UIDeadline{
		{RuleID: cID.String(), Code: "x.conditional", IsConditional: true},
		{RuleID: dID.String(), Code: "x.duration"},
		{RuleID: csID.String(), Code: "x.courtset", IsCourtSet: true},
		{RuleID: d2ID.String(), Code: "x.short"},
	}

	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)

	// Concrete rows first (sorted by duration): x.short (14d) then
	// x.duration (2mo). Then the two no-date rows, tiebroken by code:
	// x.conditional < x.courtset alphabetically.
	want := []string{"x.short", "x.duration", "x.conditional", "x.courtset"}
	for i, w := range want {
		if deadlines[i].Code != w {
			t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
		}
	}
}

// TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged guards
// the root-rule exception: top-level rules (parent_id=nil, no
// trigger_event_id) must never be sorted against each other — they
// represent distinct anchor points (SoC vs oral hearing vs decision)
// whose proceeding-sequence order is non-negotiable.
func TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged(t *testing.T) {
	rootSoCID, rootSoCRule := makeRule(t, nil, "x.soc", 0, "months")
	rootOralID, rootOralRule := makeRule(t, nil, "x.oral", 0, "months")
	rootDecID, rootDecRule := makeRule(t, nil, "x.decision", 0, "months")

	ruleByID := map[uuid.UUID]models.DeadlineRule{
		rootSoCID: rootSoCRule, rootOralID: rootOralRule, rootDecID: rootDecRule,
	}

	deadlines := []UIDeadline{
		makeDeadline(rootSoCID, "x.soc"),
		makeDeadline(rootOralID, "x.oral"),
		makeDeadline(rootDecID, "x.decision"),
	}

	sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)

	// Roots must keep their input order — they're not in the same
	// trigger group as each other.
	want := []string{"x.soc", "x.oral", "x.decision"}
	for i, w := range want {
		if deadlines[i].Code != w {
			t.Errorf("deadlines[%d].Code = %q, want %q (roots must not be sorted against each other)", i, deadlines[i].Code, w)
		}
	}
}
